※ 引述《sustainer123 (溫水佳樹的兄長大人)》之銘言: : 全局鎖(global interpreter lock,GIL) : 討論python的thread 我們就不得不提到GIL : 這邊必須先聲明 GIL不是python的特性 : 這是cpython的產物 : cpython是python的直譯器 : 假如您想避開GIL 您可以考慮其他直譯器 諸如jpython : 但cpython是大多數情況的默認環境 : 所以大多數python使用者還是得面對GIL : GIL創立的目的是為了解決多線程的安全問題 : 這邊要先介紹cpython的記憶體管理機制 : cpython有一種管理方式叫引用記數 : cpython的object的struct裡面有個東西叫ob_refcnt : 當你創建或引用變數時 ob_refcnt會+1 : 反之-1 : 當我們使用多線程時 線程會共用記憶體 : 這意味著線程會共用全局變數 : 當不同線程同時修改同個變數的引用記數 : 這就可能導致內存洩漏或內存提前被釋放 : 總之 cpython考慮線程安全 才會引進GIL : GIL的具體運作方式 : 線程執行前 必須取得GIL 取得後才能開始執行任務 : 如果線程進入IO任務或執行超過一定時間(通常15毫秒)或執行完成 : 釋放GIL : 所以一個線程會經歷三個步驟: : 1.爭取GIL : 2.執行任務 : 3.釋放GIL : 因為只有一個GIL 所以一段時間內只會有一個線程運行 : 假設是多核心的情況 其他進程本來能在其他核心運行 : 因為GIL的緣故 其他核心只能在一旁閒置 : 所以python的多線程才會被說實際是單線程 : 從執行的效率來說 有GIL的多線程甚至不如單線程 : 因為每當GIL釋放 系統需要執行線程切換 : 線程切換是保存線程的狀態 並載入下一個執行的線程 : 再者 GIL釋放時 線程會被喚醒競爭GIL : 當GIL頻繁被釋出 上述行為重複執行 : 這耗費了大量的CPU資源 : 反觀單線程就不需要執行上述行為 : 所以在CPU密集型任務 單線程優於多線程 : PY: : import time : def countdown(n): : while (n > 0): : n = n - 1 : start = time.time() : countdown(100000000) : end = time.time() : print('總時間:', end - start) : result: : 總時間: 2.3528549671173096 : py: : import time : import threading : def countdown(n): : while (n > 0): : n = n - 1 : t1 = threading.Thread(target=countdown, args=(100000000/2,)) : t2 = threading.Thread(target=countdown, args=(100000000/2,)) : start = time.time() : t1.start() : t2.start() : t1.join() : t2.join() : end = time.time() : print('總時間:', end - start) : result: : 總時間: 2.818242073059082 : 上述結果可以證明在cpu密集型任務 單線程優於多線程 : 先這樣 後面應該會寫gil爭搶的可能問題 gil實際並沒有線程安全 : 怎解決gil問題ㄅ : 累了 剩下明天再寫 : 參考資料: : https://wiki.python.org/moin/GlobalInterpreterLock : https://yhtechnote.com/global-interpreter-lock/ 接續上面 在python中 線程進入I/O任務也會釋放GIL 因為I/O任務需要等待其他系統回應 這段等待時間就會釋放I/O 其他線程就能執行任務 所以在I/O任務 多線程會優於單線程 py: import time import requests def request(url): response = requests.get(url) start = time.time() request('https://google.com') request('https://google.com') end = time.time() print('總時間:', end - start) result: 總時間: 1.0379688739776611 py: import time import threading import requests def request(url): response = requests.get(url) t1 = threading.Thread(target=request, args=('https://google.com',)) t2 = threading.Thread(target=request, args=('https://google.com',)) start = time.time() t1.start() t2.start() t1.join() t2.join() end = time.time() print('總時間:', end - start) result: 總時間: 0.7394030094146729 所以 在I/O任務使用多線程沒啥問題 線程安全 我們一開始提到 創建GIL的目的為了線程安全 但 GIL其實並沒有完全保證線程安全 因為線程可能會被迫釋放GIL 假設你的操作不是atomic operation 線程安全就會出問題 atomic operation是指在Bytecode層面上不可分的操作 我們可以使用dis看到程式碼在Bytecode層面的情況 舉例來說: import dis lst = [4, 1, 3, 2] def foo(): lst.sort() dis.dis(foo) result: 5 0 RESUME 0 6 2 LOAD_GLOBAL 0 (lst) 14 LOAD_METHOD 1 (sort) 36 PRECALL 0 40 CALL 0 50 POP_TOP 52 LOAD_CONST 0 (None) 54 RETURN_VALUE 這段程式碼是調用sort()對list排序 一開始先載入list 之後載入sort() 再來執行sort() sort()在Bytecode只有一行程式碼 這就代表這是不可分的 這代表sort()排到一半的時候不會被中斷而釋放GIL 這就叫atomic operation 對比另一個程式: import dis n = 0 def foo(): global n n += 1 dis.dis(foo) result: 4 0 RESUME 0 6 2 LOAD_GLOBAL 0 (n) 14 LOAD_CONST 1 (1) 16 BINARY_OP 13 (+=) 20 STORE_GLOBAL 0 (n) 22 LOAD_CONST 0 (None) 24 RETURN_VALUE 這段程式碼是將n+1並更新n 一開始載入全域變數n 第二步載入常數1 第三步 執行二元運算 n+1 最後把加完的數存回n 我們可以發現這裡比上一個例子多了一步 把加完的數存回n 所以有可能執行運算後被中斷 n沒有被更新 結果就可能不如我們預期 綜合上述 在非atomic operation的情況下 GIL鎖就不能保證線程安全 解決方法是自行上鎖 在讀寫可變的共用變數時加個鎖 可以避免大多數問題 結論: 上面談了很多GIL的問題 其實結論就一句話 I/O任務請用多線程 CPU密集型任務請用多進程 參考資料: https://opensource.com/article/17/4/grok-gil -- ※ 發信站: 批踢踢實業坊(ptt.org.tw), 來自: 114.136.134.162 (臺灣) ※ 文章網址: https://ptt.org.tw/Marginalman/M.1727842428.A.094
cities516: 我看完了 推 10/02 12:27
JerryChungYC: https://youtu.be/qQt7G5qhRS8 10/02 15:25