我用4年時間解決了Python GIL的一個bug……
Python部落(python.freelycode.com)組織翻譯,禁止轉載,歡迎轉發。
作為Python最關鍵的組成部分之一:GIL(全局解釋器鎖),我花了4年時間修復了其中的一個令人討厭的bug。為了修復這個bug,我不得不深挖Git的歷史,才找出26年前Guido van Rossum (龜叔,Python創立者) 所做的一處更改。那個時候,線程還是很深奧的東西。
我的故事是這樣的。
由C線程和GIL引發的致命錯誤2014年3月,Steve Dower報告了bug bpo-20891。這個bug發生在「C線程」使用Python C API時:
在Python 3.4rc3版本中,從一個非Python創建的線程中調用PyGILState_Ensure,並且完全沒有調用 PyEval_InitThreads的情況下,將產生一個致命的退出:
發生致命的Python錯誤:take_gil: tstate
我的第一個評論是:
以我之愚見,這是PyEval_InitThreads中的一個Bug。
修復PyGILState_Ensure2年的時間裡,我完全不記得這個bug了。 2016年3月,我修改了Steve的測試程序,使其與Linux兼容(該測試是為Windows編寫的)。 我成功地重現了我電腦上的錯誤,並且為PyGILState_Ensure寫了一個修復程序。
一年後,2017年11月,卡辛斯基問道:
此修複發布了嗎? 我在更新日誌中找不到...
哎呀,我又完全忘記了這個問題! 這一次,我不僅安裝了我的PyGILState_Ensure修復,還編寫了單元測試test_embed.test_bpo20891:
好的,這個bug現在已經在Python 2.7, 3.6 和master(將來的3.7)中得到解決。 在3.6和master版本中,此修復帶有單元測試。
我的主分支的修復,提交b4d1e1f7:
於是我關閉了問題bpo-20891 ...
macOS上測試發生隨機崩潰一切都很好......但一周後,我注意到我新增加的單元測試在macOS buildbots上發生了隨機崩潰。 我成功地手動重現了這個bug,第三次運行時崩潰的例子:
macOS上的test_embed.test_bpo20891在PyGILState_Ensure 中顯示有競態條件(race condition):GIL鎖本身的創建...沒有被加鎖保護! 添加一個新的鎖來檢查Python是否有GIL鎖,好像沒有意義...
我提出了PyThread_start_new_thread的一個不完整的修復:
我發現有一個修復是管用的:在PyThread_start_new_thread中調用PyEval_InitThreads。 那麼,一旦生成第二個線程就會創建GIL鎖。 當兩個線程正在運行時,GIL不能再創建。 至少,用python代碼不可以建。 如果一個線程不是由Python產生的話,此修復不能解決這個問題,但是這個線程調用了PyGILState_Ensure。
為什麼不始終創建GIL?Antoine Pitrou問了一個簡單的問題:
為什麼不在解釋器初始化時總是調用PyEval_InitThreads? 有什麼缺點嗎?
感謝git blame和git log,我發現了「按需」創建GIL的代碼,來自於26年前做出的改變!
我的猜測是,動態創建GIL的目的是為了減少GIL的「開銷」。這些GIL用於那些只使用單個Python線程的應用程序(永遠不會產生新的Python線程)。
幸運的是,Guido van Rossum在我附近,能夠對基本原理加以闡述:
是的,最初的理由是線程是深奧的,不為大多數代碼所使用,並且當時我們一定覺得:總是使用GIL會導致(微小的)速度放緩,並增加由於GIL代碼中的錯誤而導致崩潰的風險。 我很高興得知我們不再需要擔心這一點,並且可以始終對其進行初始化。
提出Py_Initialize的第二個修復
我提出了Py_Initialize的第二個修復,以便在Python啟動時始終創建GIL,並且不再「按需」,以防止出現競態條件的風險:
Nick Coghlan問我是否可以通過性能基準測試我的補丁。 我在我的PR 4700上運行pyperformance。差異至少5%:
哦,5個基準比較慢。 Python中性能退步是不受歡迎的:我們正在努力讓Python變得更快!
在聖誕節前忽略錯誤測試我沒有想到5個基準測試會變慢。 我需要進一步的調查,但時間不夠。也許是我太害羞,或者羞於承擔導致性能退步的責任。
在聖誕節假期之前,我沒有做任何決定,而test_embed.test_bpo20891在macOS buildbots上仍然是隨機失敗。 在離開兩個星期之前,我對於觸及Python的關鍵部分,即GIL,並沒有太多把握。 所以我決定,等到我回來之前,先跳過test_bpo20891。
沒有聖誕禮物給你了:Python 3.7。
運行新的基準測試,和應用於master的第二個修復在2018年1月底,我再次運行了那5個由於我的PR(Pull request)而變慢的基準測試。 我使用了CPU隔離,在我的筆記本電腦上手動運行這些基準測試:
好吧,它證實了,依照Python性能基準套件,我的第二個修復對性能沒有顯著的影響。
我決定將我的修復程序推送到master分支,提交2914bb32:
然後我在master分支上重新啟用了test_embed.test_bpo20891。
沒有適用於Python 2.7和3.6的第二個修復,抱歉!Antoine Pitrou認為,不應該合併Python 3.6的backport (註:backport是將一個軟體的補丁應用到比此補丁所對應的版本更老的版本的行為):
我不這麼認為。 人們可能已經調用PyEval_InitThreads。
Guido van Rossum也不想把這一修改做backport。 所以我只從3.6的分支中刪除了test_embed.test_bpo20891。
由於相同的原因,我沒有將我的第二個修復應用於Python 2.7。 而且,Python 2.7沒有單元測試,因為它很難backport。
至少,Python 2.7和3.6獲得了我的第一個PyGILState_Ensure修復。
結論在少數案例中,Python仍然存在一些競態條件。 當一個C線程開始使用Python API時,在創建GIL時就可以發現這樣的Bug。 我推出了第一個修復程序,但在macOS上發現了一個新的不同的競態條件。
我不得不深入研究Python GIL的歷史(1992年)。 幸運的是,Guido van Rossum也能夠闡述其基本原理。
在基準測試出現故障後,我們同意修改Python 3.7,以便始終創建GIL,而不是按需創建GIL。 該變化對性能沒有顯著的影響。
我們還決定讓Python 2.7和3.6保持不變,以防止任何回退風險:可以繼續按需創建GIL。
我花了4年的時間修復了Python GIL中的一個令人討厭的bug。 在接觸Python中如此關鍵的部分時,我從未自信滿滿。 現在,我很高興這個bug被我們甩在了身後:現在,它已經在未來的Python 3.7中完全修復了!
完整的故事見bpo-20891。 感謝幫助我解決這個Bug的所有開發人員!
英文原文:https://vstinner.github.io/python37-gil-change.html?utm_source=mybridge&utm_medium=web&utm_campaign=read_more
譯者:泰然


※00 後都在學 Python 了,而你卻還在原地打轉?
※說實話!西二旗人炫起富來更誇張!
TAG:Python部落 |