什麼是GIL?
GIL全稱global interpreter lock,全域性直譯器鎖。每一個執行緒在執行之前都需要獲取GIL許可權才可執行程式碼,也就是說在多執行緒中,實際上同一時間只有一個執行緒在執行。GIL並非是python自帶的特性,而是Cpython直譯器引入的一個概念,在Jpython(Java實現的python直譯器)中就沒有GIL。
這是一個歷史遺留問題,如今大量開發者習慣了這套機制,程式碼量也越來越多 ,已經不容易通過修改Cpython來解決這個問題。
並行與併發
在進一步瞭解GIL之前,我們先回顧一下並行與併發的概念。
並行:同一時間,可以處理多個任務,多個任務是一起被執行。
併發:同一時間,不能處理多個任務,但是可以交替處理多個任務。
舉個簡單的例子:
你邊寫作業,邊玩手機,說明你支援並行。
你寫5分鐘作業然後玩10分鐘手機,再寫5分鐘,再玩10分鐘,說明你支援併發。
他們雖然方式不同,但都指向了一個點,多工執行,目的是要提高CPU的使用效率。這裡需要注意 一點,單核CPU永遠無法實現並行,一個CPU無法同時執行多個程式,但是可以實現併發。在多執行緒執行程式碼過程中會帶來一個問題,執行緒間的資料一致性和狀態同步完整性。(補充說明:子程式在開始後,他們的執行是隨機的,並沒有先後順序)解決這個問題最簡單的方案,就是加上GIL這把全域性大鎖。因為有了GIL的存在,python中多執行緒的效率會大打折扣。甚至幾乎等於python就是單執行緒程式。
下面我們做一個python下多執行緒和單執行緒效率對比:
from threading import Thread
import time
def task(): #執行任務
i = 0
for _ in range(10000000):
i = i + 1
return True
複製程式碼
下面main1模仿的是序列執行任務,其中採用for迴圈建立執行緒主要是增加建立執行緒的時間,更準確的對比出多執行緒有GIL的差別。
def main1(): #單執行緒序列
start_time = time.time()
for tid in range(20):
t = Thread(target=task)
t.start()
t.join()
end_time = time.time()
print("單執行緒耗時: {}".format(end_time - start_time))
def main2(): #多執行緒併發
thread_array = {}
start_time = time.time()
for tid in range(20):
t = Thread(target=task)
t.start()
thread_array[tid] = t
for i in range(20):
thread_array[i].join()
end_time = time.time()
print("多執行緒耗時: {}".format(end_time - start_time))
if __name__ == '__main__':
main1()
main2()
複製程式碼
測試CPU為i7 7700k,python版本為3.7,測試結果如下:
當任務為計算密集型時,多執行緒併發對比序列區別並不大。
單執行緒耗時: 16.951716423034668
多執行緒耗時: 16.735241651535034
複製程式碼
下面我們修改一下執行任務,暫停1秒模仿程式IO操作:
def task(): #執行任務
time.sleep(1)
return True
複製程式碼
執行結果如下:
對比明顯,多執行緒在執行IO密集型效率會比序列高很多。
單執行緒耗時: 20.019601345062256
多執行緒耗時: 1.0051548480987549
複製程式碼
接下來我們對比一下多執行緒與多程式在計算密集型和IO密集型的差異:
計算密集型
# 計算密集任務
def task():
sum = 1
for i in range(100000000):
sum *= i
pass
複製程式碼
IO密集型
# IO密集任務
def task():
time.sleep(5)
pass
複製程式碼
if __name__ == '__main__':
start_time = time.time()
# 多執行緒
t1 = Thread(target=task)
t2 = Thread(target=task)
t3 = Thread(target=task)
t4 = Thread(target=task)
t5 = Thread(target=task)
t6 = Thread(target=task)
# 多程式
# t1 = Process(target=task)
# t2 = Process(target=task)
# t3 = Process(target=task)
# t4 = Process(target=task)
# t5 = Process(target=task)
# t6 = Process(target=task)
t1.start()
t2.start()
t3.start()
t4.start()
t5.start()
t6.start()
t1.join()
t2.join()
t3.join()
t4.join()
t5.join()
t6.join()
end_time = time.time()
print("多執行緒耗時: {}".format(end_time - start_time))
複製程式碼
對比結果如下:
計算密集型
多執行緒耗時: 38.91398763656616
多程式耗時: 9.934444665908813
複製程式碼
IO密集型
多執行緒程耗時: 5.002830743789673
多程式程耗時: 5.4561707973480225
複製程式碼
在計算密集型中,多程式效率幾乎碾壓多執行緒,而在IO密集型多執行緒的優勢就體現出來了,並且隨著執行緒數量越多優勢越明顯。
總結: 經過實驗對比,我們不難發現在GIL下的多執行緒也有自己的特色,在IO場景下有較好的效能,而在其他方面和單執行緒差異不大,當遇到被迫需要多執行緒的場景下,也可以考慮用多程式來代替。Python GIL是功能和效能之間權衡後的產物,它尤其存在的合理性,也有較難改變的客觀因素。
ps:本文是學習之後的思考與總結,如有不足之處望指點。