Python 多執行緒(一)
Python多執行緒(一)
執行緒
一個進程中的各個執行緒與主執行緒共享同一片數據空間,因此相對於進程,執行緒間的資訊共享與通訊更加便捷。執行緒以並發方式執行,得益於這種並行與數據共享的機制,使得多任務協作的實現更加簡單。
Python執行緒模型
Python程式碼的執行是由Python虛擬機控制。在 CPython 中,由於存在 全局解釋器鎖(GIL),同一時刻只有一個執行緒可以執行。這種限制使得python的多執行緒這像在單CPU上跑多進程,只能做到並發,無法做到並行。 。
Python虛擬機按照下面所述方式來切換執行緒
切換執行緒被放在一個互斥鎖中
-
設置GIL
-
執行某個執行緒A
-
執行下面操作之一
- 執行一定數量的A的python程式碼(位元組碼指令)
- 執行緒主動讓出控制權
-
將A的執行狀態保存,以備下次執行
-
解鎖GIL
某些 Python I/O 常式(調用了內置的作業系統 C 程式碼的那種),GIL 會在 I/O 調用前被釋放,以允許其他執行緒在 I/O 執行的時候運行。而對於那些沒有太多 I/O 操作的程式碼而言,更傾向於在該執行緒整個時間片內始終佔有處理器(和 GIL)。總而言之,I/O 密集型的 Python 程式要比計算密集型的程式碼能夠更好地利用多執行緒環境。
如果想利用多核心電腦的計算資源,推薦使用 multiprocessing
或 concurrent.futures.ProcessPoolExecutor
。
Threading模組
未引進執行緒
def func(name, t):
print(name, "開始", int(time()))
sleep(t)
print(name, "結束", int(time()))
print("程式開始執行")
start = time()
func("豬", 4)
func("牛", 4)
end = time()
print("總共運行:",int( end - start))
結果
程式開始執行
豬 開始 1618821733
豬 結束 1618821737
牛 開始 1618821737
牛 結束 1618821741
總共運行: 8
引入執行緒
方式一:創建Thread實例,傳給它一個函數
- 通過給Thread類構造函數的關鍵字參數func來定義這個執行緒將要運行哪個函數,通過args參數來傳遞func的參數。注意args是一個元組,如果func只需要一個參數時應該這樣傳參args=(arg1,)。關鍵字only參數通過kwargs來傳入。kwargs是一個字典。
from threading import Thread
from time import sleep, time
def func(name, t):
print(name, "開始", int(time()))
sleep(t)
print(name, "結束", int(time()))
T1 = Thread(target=func,args=('豬',4))
T2 = Thread(target=func,args=('牛',4))
print("主執行緒開始執行")
start = time()
T1.start() #執行緒開始執行
T2.start() #執行緒開始執行
end = time()
print("主執行緒共運行:",int( end - start))
結果
主執行緒開始執行
豬 開始 1618822143
牛 開始 1618822143
主執行緒共運行: 0
豬 結束 1618822147
牛 結束 1618822147
解釋
當調用thread實例T的start()方法時,這時會創建一個獨立的執行緒,在這個執行緒中運行實例T的run()方法.run()方法默認將args和kwargs傳入target.具體來說就是將 ( ‘豬’ , 4) 傳入 func.
當T1.start() T2.start() 調用時,會分別創建兩個獨立的執行緒,加上主執行緒一共有三個獨立執行緒在python虛擬機中運行。三個獨立執行緒以不可預測的進度運行。因此有了上面的運行結果。
注意到,當主執行緒結束後,整個程式並沒有結束。python解釋器等到所有的非守護執行緒結束之後才結束整個程式。這是threading類帶來的福利。這也是我們使用threading模組,而不使用更底層的thread模組(這裡說的不是Thread類。)的一個原因。
方式二:繼承Thread,並重新定義run方法
剛才解釋過,調用實例T的start()方法時,會在一個獨立的執行緒中運行實例的run() 方法。run的默認行為是,用構造函數中的target和kwargs來運行target。因此我們可以在子類中重新定義了run方法,在run方法中寫出我們想要並發執行的功能。
如果子類型重載了構造函數,它一定要確保在做任何事前,先發起調用基類構造器(Thread.init())。!!!!!
from threading import Thread
from time import sleep, time
def func(name, t):
print(name, "開始", int(time()))
sleep(t)
print(name, "結束", int(time()))
class MyThread(Thread):
def __init__(self,target,args=(), kwargs={}):
Thread.__init__(self)
#如果子類型重載了構造函數,它一定要確保在做任何事前,先發起調用基類構造器(Thread.__init__())。
self.target = target
self.args = args
self.kwargs = kwargs
def run(self):
self.target(*self.args,**(self.kwargs))
print("程式開始執行")
start = time()
T1 = MyThread(func,('豬',4))
T2 = MyThread(func,('牛',4))
T1.start()
T2.start()
end = time()
print("總共運行:",int( end - start))
結果
程式開始執行
豬 開始 1618824594
牛 開始 1618824594
總共運行: 0
豬 結束 1618824598
牛 結束 1618824598
全局解釋器鎖GIL
CPython 解釋器所採用的一種機制,它確保同一時刻只有一個執行緒在執行 Python bytecode。此機制通過設置對象模型(包括 dict 等重要內置類型)針對並發訪問的隱式安全簡化了 CPython 實現。給整個解釋器加鎖使得解釋器多執行緒運行更方便,其代價則是犧牲了在多處理器上的並行性。
不過,某些標準庫或第三方庫的擴展模組被設計為在執行計算密集型任務如壓縮或哈希時釋放 GIL。此外,在執行 I/O 操作時也總是會釋放 GIL。
創建一個(以更精細粒度來鎖定共享數據的)「自由執行緒」解釋器的努力從未獲得成功,因為這會犧牲在普通單處理器情況下的性能。據信克服這種性能問題的措施將導致實現變得更複雜,從而更難以維護