Python Threading 學習筆記 | 5、不一定有效率GIL

  • 2019 年 11 月 5 日
  • 筆記

0x00 關於GIL

GIL的全稱是Global Interpreter Lock(全局解釋器鎖),來源是python設計之初的考慮,為了數據安全所做的決定。

每個CPU在同一時間只能執行一個執行緒(在單核CPU下的多執行緒其實都只是並發,不是並行,並發和並行從宏觀上來講都是同時處理多路請求的概念。但並發和並行又有區別,並行是指兩個或者多個事件在同一時刻發生;而並發是指兩個或多個事件在同一時間間隔內發生。)

在Python多執行緒下,每個執行緒的執行方式如下:

1.獲取GIL

2.執行程式碼直到sleep或者是python虛擬機將其掛起。

3.釋放GIL

可見,某個執行緒想要執行,必須先拿到GIL,我們可以把GIL看作是「通行證」,並且在一個python進程中,GIL只有一個。拿不到通行證的執行緒,就不允許進入CPU執行。

也就是說儘管Python支援多執行緒,但是因為GIL的存在,使得Python還是一次性只能處理一個東西,那是不是說Python中的多執行緒就完全沒用了呢,當然不是的。

GIL往往只會影響到那些嚴重依賴CPU的程式,比如各種循環處理、計數等這種CPU密集型的程式;如果程式中大部分只會涉及到I/O,比如文件處理、網路爬蟲等這種IO密集型的程式,那麼多執行緒就能夠有效的提高效率,因為在爬蟲的時候大部分時間都在等待。

實際上,你完全可以放心的創建幾千個Python執行緒, 現代作業系統運行這麼多執行緒沒有任何壓力,沒啥可擔心的。

0x01 測試GIL

import copy  import time  import requests  import threading  from queue import Queue    def job(lists,q):     res = sum(lists)     q.put(res)      def multithreading(lists):     q = Queue()     threads_list = []       for i in range(4):        t = threading.Thread(target=job,args=(copy.copy(lists),q),name = '任務 %i' % i)        t.start()        threads_list.append(t)     for t in threads_list:        t.join()       total = 0     for _ in range(4):        total += q.get()     print('使用執行緒運算結果:',total)      def normal(lists):     total = sum(lists)     print('不使用執行緒運算結果:',total)      def req_job(i):     requests.get(i)      def req_multithreading(req_lists):     threads_list = []       for i in range(4):        t = threading.Thread(target=req_job,args=(req_lists[i],),name='爬蟲任務 %i' % i)        t.start()        threads_list.append(t)     for t in threads_list:        t.join()      def req_normal(req_lists):     for i in req_lists:        requests.get(i)      if __name__ == '__main__':     lists = list(range(1000000)) # 完成一個較大的計算     req_lists = ['https://www.teamssix.com','https://github.com/teamssix','https://me.csdn.net/qq_37683287','https://space.bilibili.com/148389186']     start_time = time.time()     multithreading(lists)     print('計算使用執行緒耗時:', time.time() - start_time,'n')       start_time = time.time()     normal(lists * 4)     print('計算不使用執行緒耗時:', time.time() - start_time,'n')       start_time = time.time()     req_multithreading(req_lists)     print('爬蟲使用執行緒耗時:', time.time() - start_time)       start_time = time.time()     req_normal(req_lists)     print('爬蟲不使用執行緒耗時:', time.time() - start_time)

運行結果:

# python 5_GIL.py  使用執行緒運算結果: 1999998000000  計算使用執行緒耗時: 0.39594030380249023    不使用執行緒運算結果: 1999998000000  計算不使用執行緒耗時: 0.3919515609741211    爬蟲使用執行緒耗時: 2.2410056591033936  爬蟲不使用執行緒耗時: 7.1159656047821045

可以看到在計算程式的程式碼中不使用執行緒和使用執行緒的運算結果是相同的,說明不使用執行緒和使用執行緒的程式都進行了一樣多次的運算,但是很明顯可以看到計算的耗時並沒有少很多,按照預期我們使用了4個執行緒,應該會快近4倍才對,這就是因為GIL在作怪。

與此同時,可以看到在使用request對一個url發起get請求的時候,使用執行緒比不使用執行緒快了3倍多,也進一步的反映出在使用Python進行爬蟲的時候,多執行緒確實可以很大程度上提高效率,但是在進行密集計算任務的時候,多執行緒就顯得很雞肋了。

程式碼項目地址:https://github.com/teamssix/Python-Threading-study-notes 參考文章: 1、https://zhuanlan.zhihu.com/p/20953544 2、https://morvanzhou.github.io/tutorials/python-basic/threading