|
這篇文章闡述Celery是如何淘汰GIL和協(xié)同程序的。 最近,我重讀了Glyph寫(xiě)的Unyielding。如果你還沒(méi)有讀過(guò),那趕緊去。我將會(huì)在下文略述它的內(nèi)容,但是,原文絕對(duì)值得一讀。 近十年我都在研究Python全局解釋器鎖,即GIL。 關(guān)于GIL,真正的問(wèn)題是異步I/O--線程就是作為處理它的簡(jiǎn)潔方法推廣的。你接收到一個(gè)請(qǐng)求,你創(chuàng)建一個(gè)線程,魔法發(fā)生了。關(guān)注是分開(kāi)的而資源是共享的。 但是在Python里,你不能高效的這樣做,因?yàn)榫€程需要爭(zhēng)奪GIL,而每個(gè)解釋器只有一個(gè)GIL,無(wú)論你的機(jī)器有多少核。所以,即使你使用頂配的英特爾酷睿i7處理器,你也不會(huì)覺(jué)得使用線程使性能有很大提升。 理論上是這樣的,現(xiàn)實(shí)可能更糟糕--Python 3.1之前的GIL實(shí)際上在多核處理器上處理多線程時(shí)性能更糟糕并且可能使你的代碼變得更慢。 異步I/O是問(wèn)題嗎? 現(xiàn)代編程中我們做的大多數(shù)任務(wù)都可以歸結(jié)為I/O,或者通常這樣回答: 例如,從數(shù)據(jù)庫(kù)取數(shù)是I/O--你等待數(shù)據(jù)的時(shí)候,系統(tǒng)可以同時(shí)做其他事,比如,服務(wù)更多請(qǐng)求。 asyncio最近向Python添加的內(nèi)容是 使用協(xié)同程序(coroutines)編寫(xiě)單線程并發(fā)代碼,通過(guò)socket和其他資源實(shí)現(xiàn)I/O復(fù)用,運(yùn)行網(wǎng)絡(luò)服務(wù)器和客戶端 這里面有一些假設(shè),我將分析一下:
首先,我們真的需要單線程并發(fā)執(zhí)行代碼嗎? 在過(guò)去10年里,我從來(lái)沒(méi)有遇見(jiàn)一個(gè)人指出“這個(gè)代碼需要并發(fā)執(zhí)行但是使用單線程。” 我認(rèn)為這里的意思其實(shí)是我們需要并發(fā)執(zhí)行,這是GIL最具爭(zhēng)議的地方--我們不能實(shí)現(xiàn)真正的并發(fā)。 我最近意識(shí)到我們真的不需要并發(fā)--稍后討論這點(diǎn),此前,我們來(lái)列出后面這兩條假設(shè),扔掉協(xié)同程序。 人們使用協(xié)同程序嗎?是的,但是不用在生產(chǎn)環(huán)境中。這可能有點(diǎn)武斷,但是我使用很多種語(yǔ)言編寫(xiě)并發(fā)程序,并且從來(lái)沒(méi)有遇到過(guò)像協(xié)同程序那么難讀的。 如果代碼意味著可讀,那么協(xié)同程序就意味著弄瞎你的眼睛。 另外,協(xié)作并發(fā)又叫協(xié)同程序在很久之前就被拋棄了。為什么? 因?yàn)閰f(xié)作程序的最基本假設(shè)是他們合作。搶先并發(fā)強(qiáng)制,或者至少?lài)L試強(qiáng)制,公平使用資源。 協(xié)同程序并沒(méi)有這樣幸運(yùn)--如果你的協(xié)同程序阻塞了,你必須等待。你的線程等待所有其他的協(xié)同程序。 那是協(xié)同程序在現(xiàn)實(shí)中的最大問(wèn)題。如果你的程序是一個(gè)shell腳本,用于計(jì)算斐波那契數(shù),那或許還行。但是在這種困境,服務(wù)器斷掉,連接超時(shí),我們不能閱讀讀取任何安裝的開(kāi)源庫(kù)的代碼。 回到并發(fā)執(zhí)行--我限制可以使用的線程數(shù)了嗎?不,一次也沒(méi)有。 我覺(jué)得我們既不需要協(xié)同程序,也不需要并發(fā)性。 我認(rèn)為我們需要的,并且值得花費(fèi)精力研究的是,非阻塞代碼。 阻塞代碼的問(wèn)題 代碼是阻塞的是說(shuō)代碼具有以下兩個(gè)特征之一:
人類(lèi)是沒(méi)有耐心的,但是在等待機(jī)器完成操作時(shí)我們看起來(lái)更急不可耐。長(zhǎng)時(shí)間等待,還是避免阻塞,其實(shí)是同一個(gè)問(wèn)題。 我們不需要阻塞一些可以很快做完的事,比如,等待一些慢的操作(數(shù)據(jù)庫(kù)請(qǐng)求)完成時(shí),可以響應(yīng)用戶。 搶先并發(fā)(線程)是解決這個(gè)問(wèn)題的好方法,有一些優(yōu)點(diǎn):
對(duì)于線程可讀性--不多說(shuō)了,他們不是最簡(jiǎn)單的可以理解的,但是絕對(duì)比協(xié)同程序好。 關(guān)于資源--那真得是從“更好一點(diǎn)”到“不可置信”轉(zhuǎn)變的實(shí)現(xiàn)細(xì)節(jié)。他們中的大部分可以使用雙核機(jī)器中的兩個(gè)核。 在經(jīng)典線程編程我們可以:
現(xiàn)在我們也可以用Python這樣做--不完成一樣,但是接近。我們可以把執(zhí)行處理數(shù)據(jù)的代碼放進(jìn)一個(gè)進(jìn)程里而不使用thread_procession_data。我當(dāng)然是在說(shuō)超級(jí)棒的multiprocession庫(kù)。 但是,那樣真的更好嗎? 我仍然需要理解幾個(gè)概念,尤其是進(jìn)程間是如何共享資源的,那看起來(lái)不是很明顯。 有更好的方法嗎? 無(wú)鎖的勝利和Celery 作為程序員我只想要不阻塞的代碼。 我不關(guān)心是通過(guò)進(jìn)程,線程,事物內(nèi)存還是魔法實(shí)現(xiàn)的。 創(chuàng)建一個(gè)工作單元,描述它的參數(shù),比如優(yōu)先級(jí),你的工作完成了。在Python世界里有一個(gè)包可以滿足你--Celery。 Celery是一個(gè)龐大的項(xiàng)目,開(kāi)始你的第一個(gè)任務(wù)前有繁雜的配置。但是一旦它開(kāi)始工作,就變得美妙。 舉個(gè)例子,工作中我有一個(gè)系統(tǒng),在各種各樣的網(wǎng)絡(luò)入口拉取一個(gè)社會(huì)股。調(diào)用API需要時(shí)間,還需要加上網(wǎng)絡(luò)連接收發(fā)數(shù)據(jù)的時(shí)間。 例如:
有了Celery,我就可以用一個(gè)任務(wù)(task)包裹update_metrics,然后這樣做:
update_metrics耗時(shí)很長(zhǎng)--但是多虧Celery我不需要考慮那些:
最重要的是:我不必再苦惱于代碼是否在執(zhí)行I/O,我是否應(yīng)該讓出,或者它是被CPU或I/O強(qiáng)迫的。 Celery可以做的事 你的問(wèn)題有:抓取1000 URLs,然后計(jì)算用戶在表格里指定的3個(gè)詞的頻率。 通常,這很難--你需要定位將要抓取的URLs。連接可能超時(shí),你需要一直等待直到所有任務(wù)完成,并且你需要以某種方式存儲(chǔ)用戶的輸入。 不使用Celery,搞清楚哪里以及如何存儲(chǔ)數(shù)據(jù)就是一個(gè)噩夢(mèng)。使用Celery我只需要任務(wù):
這樣寫(xiě)優(yōu)點(diǎn)有很多:
對(duì)了,這個(gè)例子里也有一些缺點(diǎn):
無(wú)鎖? 拋開(kāi)前面列出的問(wèn)題,對(duì)我而言,最大的問(wèn)題是細(xì)粒度控制的缺乏。隊(duì)列是一個(gè)偉大的實(shí)現(xiàn)無(wú)鎖編程的基本模型。 前面的例子假設(shè)你需要執(zhí)行一堆任務(wù)然后聚合結(jié)果--大約30行代碼的map/reduce。 但是讓我們考慮一種更加困難的情形--假設(shè)我們有一個(gè)不支持并發(fā)的任務(wù),完全是無(wú)鎖的,但是需要讀寫(xiě)而不使用阻塞。 我們應(yīng)該怎么做? 首先(這里我假設(shè)你使用Django整合)
當(dāng)然,這有一個(gè)主要問(wèn)題--我們不能同時(shí)讀取,即使可以。 結(jié)語(yǔ) 對(duì)我來(lái)講,這總結(jié)了整個(gè)GIL和協(xié)同程序/同步的爭(zhēng)論。我認(rèn)為Python核心的主要問(wèn)題是它很大程度上由C啟發(fā)。但是,在這里我認(rèn)為這是Python的缺陷。 而且我不知道這方面努力的原因。 有數(shù)百家公司運(yùn)行Python代碼作為連接邏輯(glue logic)--單線程同步代碼(看看Django多受歡迎)但是這些公司服務(wù)數(shù)以萬(wàn)計(jì)的用戶。 我認(rèn)為如果我們想要Python完全支持同步,這是方法。引入基于隊(duì)列的完全無(wú)鎖以及允許編程人員修改隊(duì)列。 Celery已經(jīng)實(shí)現(xiàn)了其中的大部分。為了在Python里擁有這些,我們需要擴(kuò)展解釋器來(lái)管理任務(wù)執(zhí)行單元和隊(duì)列,添加一些語(yǔ)法糖衣用來(lái)進(jìn)行內(nèi)嵌任務(wù)的定義和調(diào)用以便使用。 作為編程人員,我認(rèn)為我們從來(lái)不需要同步代碼。我從來(lái)不需要協(xié)同程序,也從來(lái)不需要多重I/O。 我需要的是高效表達(dá)想法和我想到它的方式的工具,抽象線程、拋棄同步、使用無(wú)鎖例程解決了這個(gè)問(wèn)題。 英文原文:http://blog./how-celery-fixed-pythons-gil-problem/ |
|
|
來(lái)自: River_LaLaLa > 《Python》