摘要:酷睿代在年取代了奔騰,主頻遠(yuǎn)低于此。該詞被敏捷開發(fā)團(tuán)隊(duì)使用較多,含義與形式會(huì)略有不同,更改已經(jīng)開始將垃圾收集器的狀態(tài)轉(zhuǎn)到解釋器,因此每個(gè)子解釋器將擁有它自己的本該如此。結(jié)論死亡了嗎對(duì)于單線程的應(yīng)用程序,仍然存活。
本文原創(chuàng)并首發(fā)于公眾號(hào)【Python貓】,未經(jīng)授權(quán),請(qǐng)勿轉(zhuǎn)載。
原文地址:https://mp.weixin.qq.com/s/8KvQemz0SWq2hw-2aBPv2Q
花下貓語: Python 中最廣為人詬病的一點(diǎn),大概就是它的 GIL 了。由于 GIL 的存在,Python 無法實(shí)現(xiàn)真正的多線程編程,因此很多人都把這視作 Python 最大的軟肋。
PEP-554 提出后(2017年9月),大伙似乎看到了一線改善的曙光。然而,GIL 真的可以被徹底殺死么,如果可以的話,它會(huì)怎么實(shí)現(xiàn)呢,為什么等了一年多還沒實(shí)現(xiàn),仍需要我們等待多長(zhǎng)時(shí)間呢?
英文 | Has the Python GIL been slain?【1】
作者 | Anthony Shaw
譯者 | 豌豆花下貓
聲明 :本文獲得原作者授權(quán)翻譯,轉(zhuǎn)載請(qǐng)保留原文出處,請(qǐng)勿用于商業(yè)或非法用途。
2003 年初,Intel 公司推出了全新的奔騰 4 “HT” 處理器,該處理器的主頻(譯注:CPU 內(nèi)核工作的時(shí)鐘頻率)為 3 GHz,采用了“超線程”技術(shù)。
在接下來的幾年中,Intel 和 AMD 激烈競(jìng)爭(zhēng),通過提高總線速度、L2 緩存大小和減小芯片尺寸以最大限度地減少延遲,努力地實(shí)現(xiàn)最佳的臺(tái)式機(jī)性能。3Ghz 的 HT 在 2004 年被“Prescott”的 580 型號(hào)取代,該型號(hào)的主頻高達(dá) 4 GHz。
似乎提升性能的最好方法就是提高處理器的主頻,但 CPU 卻受到高功耗和散熱會(huì)影響全球變暖的困擾。
你電腦上有 4Ghz 的 CPU 嗎?不太可能,因?yàn)樾阅艿那斑M(jìn)方式是更高的總線速度和更多的內(nèi)核。Intel 酷睿 2 代在 2006 年取代了奔騰 4 ,主頻遠(yuǎn)低于此。
除了發(fā)布消費(fèi)級(jí)的多核 CPU,2006 年還發(fā)生了其它事情,Python 2.5 發(fā)布了!Python 2.5 帶來了人見人愛的 with 語句的 beta 版本 。
在使用 Intel 的酷睿 2 或 AMD 的 Athlon X2 時(shí),Python 2.5 有一個(gè)重要的限制——GIL 。
什么是 GIL?GIL 即全局解釋器鎖(Global Interpreter Lock),是 Python 解釋器中的一個(gè)布爾值,受到互斥保護(hù)。這個(gè)鎖被 CPython 中的核心字節(jié)碼用來評(píng)估循環(huán),并調(diào)節(jié)用來執(zhí)行語句的當(dāng)前線程。
CPython 支持在單個(gè)解釋器中使用多線程,但線程們必須獲得 GIL 的使用權(quán)才能執(zhí)行操作碼(做低級(jí)操作)。這樣做的好處是,Python 開發(fā)人員在編寫異步代碼或多線程代碼時(shí),完全不必操心如何獲取變量上的鎖,也不需擔(dān)心進(jìn)程因?yàn)樗梨i而崩潰。
GIL 使 Python 中的多線程編程變得簡(jiǎn)單。
GIL 還意味著雖然 CPython 可以是多線程的,但在任何給定的時(shí)間里只能執(zhí)行 1 個(gè)線程。這意味著你的四核 CPU 會(huì)像上圖一樣工作 (減去藍(lán)屏,但愿如此)。
當(dāng)前版本的 GIL 是在2009年編寫的 【2】,用于支持異步功能,幾乎沒被改動(dòng)地存活了下來,即使曾經(jīng)多次試圖刪除它或減少對(duì)它的依賴。
所有提議移除 GIL 的訴求是,它不應(yīng)該降低單線程代碼的性能。任何曾在 2003 年啟用超線程(Hyper-Threading)的人都會(huì)明白為什么 這很重要 【3】。
在 CPython 中避免使用 GIL如果你想在 CPython 中使用真正的并發(fā)代碼,則必須使用多進(jìn)程。
在 CPython 2.6 中,標(biāo)準(zhǔn)庫里增加了 multiprocessing 模塊。multiprocessing 是 CPython 大量產(chǎn)生的進(jìn)程的包裝器(每個(gè)進(jìn)程都有自己的GIL)——
from multiprocessing import Process def f(name): print "hello", name if __name__ == "__main__": p = Process(target=f, args=("bob",)) p.start() p.join()
進(jìn)程可以從主進(jìn)程中“孵出”,通過編譯好的 Python 模塊或函數(shù)發(fā)送命令,然后重新納入主進(jìn)程。
multiprocessing 模塊還支持通過隊(duì)列或管道共享變量。它有一個(gè) Lock 對(duì)象,用于鎖定主進(jìn)程中的對(duì)象,以便其它進(jìn)程能夠?qū)懭搿?/p>
多進(jìn)程有一個(gè)主要的缺陷:它在時(shí)間和內(nèi)存使用方面的開銷很大。CPython 的啟動(dòng)時(shí)間,即使沒有非站點(diǎn)(no-site),也是 100-200ms(參見 這個(gè)鏈接 【4】)。
因此,你可以在 CPython 中使用并發(fā)代碼,但是你必須仔細(xì)規(guī)劃那些長(zhǎng)時(shí)間運(yùn)行的進(jìn)程,這些進(jìn)程之間極少共享對(duì)象。
另一種替代方案是使用像 Twisted 這樣的三方庫。
PEP-554 與 GIL 的死亡?小結(jié)一下,CPython 中使用多線程很容易,但它并不是真正的并發(fā),多進(jìn)程雖然是并發(fā)的,但開銷卻極大。
有沒有更好的方案呢?
繞過 GIL 的線索就在其名稱中,全局 解釋器 鎖是全局解釋器狀態(tài)的一部分。 CPython 的進(jìn)程可以有多個(gè)解釋器,因此可以有多個(gè)鎖,但是此功能很少使用,因?yàn)樗煌ㄟ^ C-API 公開。
在為 CPython 3.8 提出的特性中有個(gè) PEP-554,提議實(shí)現(xiàn)子解釋器(sub-interpreter),以及在標(biāo)準(zhǔn)庫中提供一個(gè)新的帶有 API 的 interpreters 模塊。
這樣就可以在 Python 的單個(gè)進(jìn)程中創(chuàng)建出多個(gè)解釋器。Python 3.8 的另一個(gè)改動(dòng)是解釋器都將擁有多帶帶的 GIL ——
因?yàn)榻忉屍鞯臓顟B(tài)包含內(nèi)存分配競(jìng)技場(chǎng)(memory allocation arena),即所有指向 Python 對(duì)象(局地和全局)的指針的集合,所以 PEP-554 中的子解釋器無法訪問其它解釋器的全局變量。
與多進(jìn)程類似,在解釋器之間共享對(duì)象的方法是采用 IPC 的某種形式(網(wǎng)絡(luò)、磁盤或共享內(nèi)存)來做序列化。在 Python 中有許多方法可以序列化對(duì)象,例如 marshal 模塊、 pickle 模塊、以及像 json 和 simplexml 這樣更標(biāo)準(zhǔn)化的方法 。這些方法褒貶不一,但無一例外會(huì)造成額外的開銷。
最佳方案是開辟一塊共享的可變的內(nèi)存空間,由主進(jìn)程來控制。這樣的話,對(duì)象可以從主解釋器發(fā)送,并由其它解釋器接收。這將是 PyObject 指針的內(nèi)存管理空間,每個(gè)解釋器都可以訪問它,同時(shí)由主進(jìn)程擁有對(duì)鎖的控制權(quán)。
這樣的 API 仍在制定中,但它可能如下所示:
import _xxsubinterpreters as interpreters import threading import textwrap as tw import marshal # Create a sub-interpreter interpid = interpreters.create() # If you had a function that generated some data arry = list(range(0,100)) # Create a channel channel_id = interpreters.channel_create() # Pre-populate the interpreter with a module interpreters.run_string(interpid, "import marshal; import _xxsubinterpreters as interpreters") # Define a def run(interpid, channel_id): interpreters.run_string(interpid, tw.dedent(""" arry_raw = interpreters.channel_recv(channel_id) arry = marshal.loads(arry_raw) result = [1,2,3,4,5] # where you would do some calculating result_raw = marshal.dumps(result) interpreters.channel_send(channel_id, result_raw) """), shared=dict( channel_id=channel_id ), ) inp = marshal.dumps(arry) interpreters.channel_send(channel_id, inp) # Run inside a thread t = threading.Thread(target=run, args=(interpid, channel_id)) t.start() # Sub interpreter will process. Feel free to do anything else now. output = interpreters.channel_recv(channel_id) interpreters.channel_release(channel_id) output_arry = marshal.loads(output) print(output_arry)
此示例使用了 numpy ,并通過使用 marshal 模塊對(duì)其進(jìn)行序列化來在通道上發(fā)送 numpy 數(shù)組 ,然后由子解釋器來處理數(shù)據(jù)(在多帶帶的 GIL 上),因此這會(huì)是一個(gè)計(jì)算密集型(CPU-bound)的并發(fā)問題,適合用子解釋器來處理。
這看起來效率低下marshal 模塊相當(dāng)快,但仍不如直接從內(nèi)存中共享對(duì)象那樣快。
PEP-574 提出了一種新的 pickle 【5】協(xié)議(v5),它支持將內(nèi)存緩沖區(qū)與 pickle 流的其余部分分開處理。對(duì)于大型數(shù)據(jù)對(duì)象,將它們一次性序列化,再由子解釋器反序列化,這會(huì)增加很多開銷。
新的 API 可以( 假想 ,并沒有合入)像這樣提供接口:
import _xxsubinterpreters as interpreters import threading import textwrap as tw import pickle # Create a sub-interpreter interpid = interpreters.create() # If you had a function that generated a numpy array arry = [5,4,3,2,1] # Create a channel channel_id = interpreters.channel_create() # Pre-populate the interpreter with a module interpreters.run_string(interpid, "import pickle; import _xxsubinterpreters as interpreters") buffers=[] # Define a def run(interpid, channel_id): interpreters.run_string(interpid, tw.dedent(""" arry_raw = interpreters.channel_recv(channel_id) arry = pickle.loads(arry_raw) print(f"Got: {arry}") result = arry[::-1] result_raw = pickle.dumps(result, protocol=5) interpreters.channel_send(channel_id, result_raw) """), shared=dict( channel_id=channel_id, ), ) input = pickle.dumps(arry, protocol=5, buffer_callback=buffers.append) interpreters.channel_send(channel_id, input) # Run inside a thread t = threading.Thread(target=run, args=(interpid, channel_id)) t.start() # Sub interpreter will process. Feel free to do anything else now. output = interpreters.channel_recv(channel_id) interpreters.channel_release(channel_id) output_arry = pickle.loads(output) print(f"Got back: {output_arry}")這看起來像極了很多樣板
確實(shí),這個(gè)例子使用的是低級(jí)的子解釋器 API。如果你使用了多進(jìn)程庫,你將會(huì)發(fā)現(xiàn)一些問題。它不像 threading 那么簡(jiǎn)單,你不能想著在不同的解釋器中使用同一串輸入來運(yùn)行同一個(gè)函數(shù)(目前還不行)。
一旦合入了這個(gè) PEP,我認(rèn)為 PyPi 中的其它一些 API 也會(huì)采用它。
子解釋器需要多少開銷?簡(jiǎn)版回答 :大于一個(gè)線程,少于一個(gè)進(jìn)程。
詳版回答 :解釋器有自己的狀態(tài),因此雖然 PEP-554 可以使創(chuàng)建子解釋器變得方便,但它還需要克隆并初始化以下內(nèi)容:
在 main 命名空間與 importlib 中的模塊
sys 字典的內(nèi)容
內(nèi)置的方法(print、assert等等)
線程
核心配置
核心配置可以很容易地從內(nèi)存克隆,但導(dǎo)入的模塊并不那么簡(jiǎn)單。在 Python 中導(dǎo)入模塊的速度很慢,因此,如果每次創(chuàng)建子解釋器都意味著要將模塊導(dǎo)入另一個(gè)命名空間,那么收益就會(huì)減少。
那么 asyncio 呢?標(biāo)準(zhǔn)庫中 asyncio 事件循環(huán)的當(dāng)前實(shí)現(xiàn)是創(chuàng)建需要求值的幀(frame),但在主解釋器中共享狀態(tài)(因此共享 GIL)。
在 PEP-554 被合入后,很可能是在 Python 3.9,事件循環(huán)的替代實(shí)現(xiàn) 可能 是這樣(盡管還沒有人這樣干):在子解釋器內(nèi)運(yùn)行 async 方法,因此會(huì)是并發(fā)的。
聽起來不錯(cuò),發(fā)貨吧!額,還不可以。
因?yàn)?CPython 已經(jīng)使用單解釋器的實(shí)現(xiàn)方案很長(zhǎng)時(shí)間了,所以代碼庫的許多地方都在使用“運(yùn)行時(shí)狀態(tài)”(Runtime State)而不是“解釋器狀態(tài)”(Interpreter State),所以假如要將當(dāng)前的 PEP-554 合入的話,將會(huì)導(dǎo)致很多問題。
例如,垃圾收集器(在 3.7 版本前)的狀態(tài)就屬于運(yùn)行時(shí)。
在 PyCon sprint 期間(譯注:PyCon 是由 Python 社區(qū)舉辦的大型活動(dòng),作者指的是官方剛在美國(guó)舉辦的這場(chǎng),時(shí)間是2019年5月1日至5月9日。sprint 是為期 1-4 天的活動(dòng),開發(fā)者們自愿加入某個(gè)項(xiàng)目,進(jìn)行“沖刺”開發(fā)。該詞被敏捷開發(fā)團(tuán)隊(duì)使用較多,含義與形式會(huì)略有不同),更改已經(jīng)開始 【6】將垃圾收集器的狀態(tài)轉(zhuǎn)到解釋器,因此每個(gè)子解釋器將擁有它自己的 GC(本該如此)。
另一個(gè)問題是在 CPython 代碼庫和許多 C 擴(kuò)展中仍殘存著一些“全局”變量。因此,當(dāng)人們突然開始正確地編寫并發(fā)代碼時(shí),我們可能會(huì)遭遇到一些問題。
還有一個(gè)問題是文件句柄屬于進(jìn)程,因此當(dāng)你在一個(gè)解釋器中讀寫一個(gè)文件時(shí),子解釋器將無法訪問該文件(不對(duì) CPython 作進(jìn)一步更改的話)。
簡(jiǎn)而言之,還有許多其它事情需要解決。
結(jié)論:GIL 死亡了嗎?對(duì)于單線程的應(yīng)用程序,GIL 仍然存活。因此,即便是合并了 PEP-554,如果你有單線程的代碼,它也不會(huì)突然變成并發(fā)的。
如果你想在 Python 3.8 中使用并發(fā)代碼,那么你就會(huì)遇到計(jì)算密集型的并發(fā)問題,那么這可能是張入場(chǎng)券!
什么時(shí)候?Pickle v5 和用于多進(jìn)程的共享內(nèi)存可能是在 Python 3.8(2019 年 10 月)實(shí)現(xiàn),子解釋器將介于 3.8 和 3.9 之間。
如果你現(xiàn)在想要使用我的示例,我已經(jīng)構(gòu)建了一個(gè)分支,其中包含所有 必要的代碼 【7】
References[1] Has the Python GIL been slain? https://hackernoon.com/has-th...
[2] 是在2009年編寫的: https://github.com/python/cpy...
[3] 這很重要: https://arstechnica.com/featu...
[4] 這個(gè)鏈接 : https://hackernoon.com/which-...
[5] PEP-574 提出了一種新的 pickle : https://www.python.org/dev/pe...
[6] 更改已經(jīng)開始: https://github.com/python/cpy...
[7] 必要的代碼 : https://github.com/tonybalone...
公眾號(hào)【Python貓】, 本號(hào)連載優(yōu)質(zhì)的系列文章,有喵星哲學(xué)貓系列、Python進(jìn)階系列、好書推薦系列、技術(shù)寫作、優(yōu)質(zhì)英文推薦與翻譯等等,歡迎關(guān)注哦。后臺(tái)回復(fù)“愛學(xué)習(xí)”,免費(fèi)獲得一份學(xué)習(xí)大禮包。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/43842.html
摘要:酷睿代在年取代了奔騰,主頻遠(yuǎn)低于此。該詞被敏捷開發(fā)團(tuán)隊(duì)使用較多,含義與形式會(huì)略有不同,更改已經(jīng)開始將垃圾收集器的狀態(tài)轉(zhuǎn)到解釋器,因此每個(gè)子解釋器將擁有它自己的本該如此。結(jié)論死亡了嗎對(duì)于單線程的應(yīng)用程序,仍然存活。showImg(https://user-gold-cdn.xitu.io/2019/5/19/16ad09f554fdf443); 本文原創(chuàng)并首發(fā)于公眾號(hào)【Python貓】,未經(jīng)授...
摘要:酷睿代在年取代了奔騰,主頻遠(yuǎn)低于此。該詞被敏捷開發(fā)團(tuán)隊(duì)使用較多,含義與形式會(huì)略有不同,更改已經(jīng)開始將垃圾收集器的狀態(tài)轉(zhuǎn)到解釋器,因此每個(gè)子解釋器將擁有它自己的本該如此。結(jié)論死亡了嗎對(duì)于單線程的應(yīng)用程序,仍然存活。showImg(https://user-gold-cdn.xitu.io/2019/5/19/16ad09f554fdf443); 本文原創(chuàng)并首發(fā)于公眾號(hào)【Python貓】,未經(jīng)授...
摘要:二這些內(nèi)容都很有意思,本文唯獨(dú)想聊聊它內(nèi)置電池。這樣做的預(yù)期效果是內(nèi)置電池會(huì)變得輕量小型化高質(zhì)量,同時(shí)三方庫的生態(tài)系統(tǒng)也能得到進(jìn)化。目前,該仍處于草案狀態(tài),但已基本成為社區(qū)共識(shí)。 showImg(https://segmentfault.com/img/remote/1460000019489233?w=1880&h=1253); 本文原創(chuàng)并首發(fā)于公眾號(hào)【Python貓】,未經(jīng)授權(quán),請(qǐng)...
摘要:二這些內(nèi)容都很有意思,本文唯獨(dú)想聊聊它內(nèi)置電池。這樣做的預(yù)期效果是內(nèi)置電池會(huì)變得輕量小型化高質(zhì)量,同時(shí)三方庫的生態(tài)系統(tǒng)也能得到進(jìn)化。目前,該仍處于草案狀態(tài),但已基本成為社區(qū)共識(shí)。 showImg(https://segmentfault.com/img/remote/1460000019489233?w=1880&h=1253); 本文原創(chuàng)并首發(fā)于公眾號(hào)【Python貓】,未經(jīng)授權(quán),請(qǐng)...
閱讀 1623·2021-11-22 14:45
閱讀 1074·2021-11-17 09:33
閱讀 3327·2021-09-02 09:48
閱讀 974·2019-08-30 15:54
閱讀 2770·2019-08-30 15:53
閱讀 2558·2019-08-30 12:54
閱讀 2248·2019-08-29 12:37
閱讀 2427·2019-08-26 13:58