既然python的多線程是"偽多線程",那么多線程訪問共享資源的時候,還需要線程鎖嗎


 

最近處理的大多數任務都是基於python的多線程實現的,然而使用python逃避不開的一個話題就是,python的GIL(的全稱是 Global Interpreter Lock)全局解釋器鎖是單線程的,那么是不是意味着python的多線程也是串行的?多線程對共享資源的使用就不需要鎖(線程鎖)了?

筆者一開始也是這么誤解:
既然python解釋器的鎖是單線程的===》那么經過解釋器生成的線程,是輪訓執行的(相當於單線程)===》然后推斷出線程訪問共享資源的時候不需要加線程鎖?
既然不需要加線程鎖,那么python為什么會存在線程鎖?這明顯是一個矛盾的問題,以上推論純屬YY,上述結論是不正確的。

其實上述推斷從第二步就開始錯誤了,其實某些時候,看似合理的推斷,是完成不成立的。
先做個小demo看一下效果,下面代碼很簡單,線程方法內部輪訓設置全局變量遞增,然后其多個線程調用該方法,線程方法在操作全局變量的時候暫時不適用線程鎖

#! /usr/bin/python
# -*- coding: utf-8 -*-
from threading import Thread,Lock

g_var = 0
lock = Lock()


def thread_tasks():
   for i in range(100000):
       global g_var
       #lock.acquire()
       g_var = g_var+1
       #lock.release()


if __name__ == "__main__":

    t_list = []
    for i in range(100):
        t = Thread(target=thread_tasks, args=[])
        t_list.append(t)
    for t in t_list:
        t.setDaemon(True)
        t.start()
    for t in t_list:
        t.join()

    print('thread execute finish')
    print('global variable g_var is ' + str(g_var))

理論上出來的結果,最終全局變量的結果就是線程數*每個線程循環的次數,這里線程數是100,線程方法內部循環100000次,最終的結果“理論上”是10,000,000。
能動手的絕對不動嘴,實際執行下來發現並不是這樣的,甚至會出現各種意料之外的答案,原因何在?

這篇《也許你對 Python GIL 鎖的理解是 錯的》文章提到這個問題,理論上也比較簡單

然后再加上線程鎖,看看效果(需要注意的是:開啟線程鎖之后,不應該無腦開啟太多的線程,因為這個線程鎖會導致線程之間嚴重爭用問題)

關於python的多線程無法對CPU使用充分的問題,筆者是12核的機器,在跑上述代碼的時候,CPU使用情況如下,鑒於對python了解的不夠深入,不知道怎么解釋這種“python單位時間內只能使用同一個CPU”的問題

 

 

關於python多線程使用,根據筆者的測試總結,也有一些思考

1,如果是遠程任務,就好比上述代碼thread_tasks中執行的是一個遠程服務(比如遠程數據庫訪問什么的),完全可以達到“並發”的效果,因為本地CPU僅負責啟動線程,線程在遠程服務端一樣可以做到並發效果,比如多線程壓測遠程訪問數據庫的時候,一樣可以打爆遠程數據庫服務器的CPU。
2,如果本地任務,且本地任務屬於非CPU密集型任務,比如IO操作,或者網絡讀寫等等,多線程一樣可以達到並發效果,因為瓶頸並不在CPU使用上(整體效率不是增加CPU的使用就可以提升的)
3,如果本地任務,且本地任務屬於CPU密集型任務,此時python的多線程是無法發揮其功效的(多進程不香么,此時可以考慮多進程並發)。

 

關於python的多進程並發模型,也非常簡單,相比線程鎖,進程之間的通訊,一個內置的queue就完事了,如下是模擬多進程並法實現上述計算。

from multiprocessing import Process, Pool, Queue
from threading import Thread, Lock
import time
import progressbar
import math

g_var = 0
lock = Lock()


def thread_tasks(q):
    for _ in range(100000):
        q.put(1)


def consumer_q(q):
    global g_var
    p = progressbar.ProgressBar()
    p.start()
    while True:
        var = q.get(True)
        g_var = g_var + var
        try:
            p.update(int((g_var / (100 * 100000 - 1)) * 100))
        except Exception as ex:
            pass
    p.finish()


if __name__ == "__main__":

    q = Queue()
    p_list = []

    p_consumer = Process(target=consumer_q, args=[q, ])
    p_consumer.start()
    for i in range(50):
        p = Process(target=thread_tasks, args=[q, ])
        p_list.append(p)
    for p in p_list:
        p.start()
    for p in p_list:
        p.join()

    time.sleep(1)
    print('\n sub_process execute finish')
    p_consumer.terminate()

 

這里非常客觀地評價了python的多進程和多線程並發對比:https://www.cnblogs.com/massquantity/p/10357898.html

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM