最近處理的大多數任務都是基於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