[asyncio隨記一]asyncio的實(shí)現(xiàn)原理和關(guān)鍵源碼分析

? ? 最近在python3.7上用asyncio做項(xiàng)目,實(shí)現(xiàn)web的服務(wù)端贬蛙,一邊從GitHub和StackOverflow上抄代碼,一邊在看asyncio相關(guān)的源碼,所學(xué)所思豆赏,姑且寫在這里。

為什么會(huì)出現(xiàn)協(xié)程(coroutine)這種設(shè)計(jì)?

? ? 多線程(thread)也是同時(shí)執(zhí)行多個(gè)任務(wù)的一種設(shè)計(jì)掷邦,為什么有了多線程白胀,我們還要設(shè)計(jì)協(xié)程,它有什么不同呢耙饰?

? ? 首先纹笼,多線程的目的是什么?

? ? 一種說法苟跪,利用多核的性能,讓代碼占用盡可能的計(jì)算資源蔓涧,運(yùn)行快一點(diǎn)件已。這算是個(gè)原因吧,我們簡稱為并行(parallelism)元暴。在這里篷扩,我還不想摳字眼討論并發(fā)(cocurrent)和并行。

? ? 另一種說法茉盏,我們程序有些任務(wù)是cpu密集型(邏輯計(jì)算比較多)鉴未,有些任務(wù)是IO密集型(讀寫文件或網(wǎng)絡(luò)比較多),如果遇到IO密集型計(jì)算鸠姨,比如從網(wǎng)站上下載一個(gè)大文件铜秆,這時(shí)候如果是阻塞下載,并且只有一個(gè)線程的話讶迁,程序的其他邏輯就無法執(zhí)行连茧,從這個(gè)程序的角度講,他沒能很好的占有cpu的資源巍糯,白白地等待下載的結(jié)束啸驯。如果開了另外一個(gè)線程的話,這個(gè)下載線程雖然阻塞掉了祟峦,但是別的線程依然可以跑別的邏輯罚斗,就可以更充分的占有cpu的資源。有人稱同時(shí)執(zhí)行不同的任務(wù)為并發(fā)宅楞。

(TODO针姿? 圖1 任務(wù)調(diào)度示意圖)

? ? 同樣針對(duì)上面第二種問題,我們換個(gè)角度表述咱筛,我們有一個(gè)IO密集型任務(wù)搓幌,還有一個(gè)cpu密集型任務(wù),我們想有效的運(yùn)行這兩個(gè)任務(wù)迅箩,不讓IO密集型任務(wù)阻塞了cpu密集型任務(wù)溉愁,所以我們構(gòu)造了一個(gè)任務(wù)調(diào)度器,當(dāng)IO密集任務(wù)開始從網(wǎng)站上下載大文件的時(shí)候,我們把他從調(diào)度器上暫時(shí)移開拐揭,后面再去檢查他撤蟆,讓調(diào)度器去執(zhí)行cpu密集任務(wù),在此期間我們可能會(huì)時(shí)不時(shí)的去檢查IO任務(wù)有沒有下載完畢(也可能是被動(dòng)通知堂污,如軟件驅(qū)動(dòng)的中斷)家肯,如果發(fā)現(xiàn)完畢了,調(diào)度器可能會(huì)暫停當(dāng)前的cpu密集任務(wù)盟猖,轉(zhuǎn)而繼續(xù)執(zhí)行IO密集任務(wù)的后續(xù)工作讨衣。在多線程環(huán)境下,這個(gè)調(diào)度器就是操作系統(tǒng)式镐,兩個(gè)任務(wù)是兩個(gè)線程反镇。而協(xié)程,就是這個(gè)思路娘汞,只不過設(shè)計(jì)的更加極端一點(diǎn)歹茶,我們?cè)诙嗑€程環(huán)境下,幾乎不可能手動(dòng)地控制先執(zhí)行什么線程你弦,再執(zhí)行什么線程(只是操作系統(tǒng)的調(diào)度工作)惊豺,而如果我們專門寫了一個(gè)任務(wù)調(diào)度器,我們自己實(shí)現(xiàn)應(yīng)用層面的調(diào)度算法禽作,就可能實(shí)現(xiàn)先執(zhí)行什么任務(wù)尸昧,再執(zhí)行什么任務(wù),什么時(shí)候暫停一個(gè)任務(wù)领迈,什么時(shí)候恢復(fù)運(yùn)行這個(gè)任務(wù)彻磁。這個(gè)可控的調(diào)度機(jī)制,就是協(xié)程狸捅。他的初始目的就是實(shí)現(xiàn)任務(wù)的并發(fā)衷蜓,只不過想更加精細(xì)地控制任務(wù)的暫停和繼續(xù)。協(xié)程是種設(shè)計(jì)思想尘喝,線程是計(jì)算機(jī)實(shí)現(xiàn)多任務(wù)的一種工程機(jī)制磁浇,線程可以用于實(shí)現(xiàn)協(xié)程。

asyncio的模樣

? ? 我們先杜撰一段代碼應(yīng)景朽褪。

圖2 調(diào)度器和兩個(gè)任務(wù)

????如圖2所示置吓,我們用async def定義了兩個(gè)函數(shù),一個(gè)下載大文件缔赠,一個(gè)做一些邏輯運(yùn)算衍锚。然后我們實(shí)現(xiàn)了一個(gè)調(diào)度函數(shù),在調(diào)度函數(shù)里嗤堰,我們分別用兩個(gè)任務(wù)函數(shù)創(chuàng)建了兩個(gè)任務(wù)戴质,加入到asyncio的event loop里面,接著,我們運(yùn)行這個(gè)event loop告匠,這樣戈抄,兩個(gè)任務(wù)就開始執(zhí)行起來了。注意后专,async with和await的時(shí)候划鸽,都是執(zhí)行一個(gè)異步函數(shù)的過程,這個(gè)時(shí)候戚哎,當(dāng)前任務(wù)會(huì)主動(dòng)讓出event loop裸诽,去后臺(tái)執(zhí)行一些網(wǎng)絡(luò)IO,event loop會(huì)選擇自己等待隊(duì)列的任務(wù)繼續(xù)執(zhí)行建瘫。等原來網(wǎng)絡(luò)IO的任務(wù)結(jié)束網(wǎng)絡(luò)IO崭捍,他會(huì)重新加入到event loop的等待隊(duì)列,等待其他任務(wù)主動(dòng)讓出event loop啰脚,被動(dòng)等待調(diào)度。比如self_play在執(zhí)行await asyncio.sleep(3)的時(shí)候會(huì)主動(dòng)讓出event loop实夹。

????上面我特別強(qiáng)調(diào)了主動(dòng)讓出event loop橄浓,這是協(xié)程的核心思想,如果一個(gè)任務(wù)沒有任何await或async with邏輯亮航,那么它一旦執(zhí)行荸实,別的任務(wù)再也沒有機(jī)會(huì)被調(diào)度到。比如缴淋,我們?nèi)绻サ魋elf_play的await語句准给,整個(gè)event loop將永遠(yuǎn)被self_play所占用,其他任務(wù)再也沒有機(jī)會(huì)執(zhí)行重抖,整個(gè)輸出只有左右右手慢動(dòng)作了露氮。總之钟沛,爸爸不給畔规,你不能搶。這一點(diǎn)和樸素的多線程很不一樣恨统。

asyncio實(shí)現(xiàn)原理推測

????從上面的介紹叁扫,我們可以大概猜出,asyncio主要有一個(gè)任務(wù)調(diào)度器(event loop)畜埋,然后可以用async def定義異步函數(shù)作為任務(wù)邏輯莫绣,通過create_task接口把任務(wù)掛到event loop上。event loop的運(yùn)行過程應(yīng)該是個(gè)不停循環(huán)的過程悠鞍,不停查看等待類別有沒有可以執(zhí)行的任務(wù)对室,如果有的話執(zhí)行任務(wù),直到碰到await之類的主動(dòng)讓出event loop的函數(shù),如此反復(fù)软驰。

(TODO涧窒? 圖3 event loop調(diào)度示意圖)

asyncio源碼分析

? ? 更進(jìn)一步的問,evnet loop大致是怎么實(shí)現(xiàn)的呢锭亏?怎么進(jìn)行調(diào)度的呢纠吴?

? ? 我們順藤摸瓜,在asyncio/base_events.py里面我們看到了create_task的源碼實(shí)現(xiàn)慧瘤,代碼的關(guān)鍵是Task的構(gòu)造戴已,傳了一個(gè)event loop(loop參數(shù))進(jìn)去,也就是在這個(gè)時(shí)候锅减,task注冊(cè)到了event loop上面糖儡。注冊(cè)過程是c實(shí)現(xiàn)的(見文末附錄1),但本質(zhì)上都是通過event loop的call_soon()怔匣。

圖4 create_task的實(shí)現(xiàn)

? ? 圖5握联,是run_forever的實(shí)現(xiàn),基本上是不停的在循環(huán)每瞒,然后每一個(gè)循環(huán)執(zhí)行一幀(_run_once)金闽。

圖5 run_forever()的實(shí)現(xiàn)

? ? 圖6是每一幀的代碼實(shí)現(xiàn),基本上是在調(diào)度隊(duì)列里找到這一幀應(yīng)該執(zhí)行的任務(wù)(任務(wù)最終注冊(cè)在event loop的結(jié)構(gòu)是Handle剿骨,通過call_soon()實(shí)現(xiàn))代芜,直接_run()。

圖6 event loop每一幀的邏輯實(shí)現(xiàn)

? ? event loop的call_soon浓利,是注冊(cè)任務(wù)時(shí)使用的挤庇,字面意思是下一幀執(zhí)行當(dāng)前注冊(cè)的任務(wù)。它的本質(zhì)就是把當(dāng)前任務(wù)封裝成Handle贷掖,放到_ready里面嫡秕,如圖7所示。

圖7 注冊(cè)任務(wù)的最終實(shí)現(xiàn)

? ? 調(diào)度隊(duì)列是event驅(qū)動(dòng)形成的羽资,這也是為什么asyncio的核心叫做event loop淘菩。這部分代碼同樣也在_run_once()里面,見圖7屠升,這個(gè)select就是某種多路復(fù)用機(jī)制潮改,比如select,epoll和iocp腹暖。

圖8 event loop處理消息流程

? ? 圖8給出了select機(jī)制下的selector.select實(shí)現(xiàn)汇在,看起來是不是有點(diǎn)熟悉啊。消息處理相關(guān)我們后續(xù)在常用接口里會(huì)再次提到脏答。

圖9 select模式下的消息歸集

總結(jié)一下asyncio的實(shí)現(xiàn)思路

????有一個(gè)任務(wù)調(diào)度器event loop糕殉,我們可以把需要執(zhí)行的coroutine打包成task加入到event loop的調(diào)度列表里面(以Handle形式)亩鬼。

????在event loop的每個(gè)幀里面,它會(huì)檢查需要執(zhí)行那些task阿蝶,然后運(yùn)行這些task雳锋,可能拿到最終結(jié)果,也可能執(zhí)行一半繼續(xù)await別的任務(wù)羡洁,任務(wù)之間互相wait玷过,通過回調(diào)來把任務(wù)串聯(lián)起來(后面常用接口會(huì)繼續(xù)深入介紹,實(shí)現(xiàn)細(xì)節(jié)見附錄2)筑煮。

????任務(wù)可能會(huì)依賴別的IO消息辛蚊,在每一幀,event loop都會(huì)用selector處理相應(yīng)的消息真仲,執(zhí)行相應(yīng)的callback函數(shù)袋马。

? ? 我們當(dāng)前的介紹里,只有一個(gè)event loop秸应,這個(gè)event loop跑在主線程里面虑凛。當(dāng)然,event loop還可以開線程池處理別的任務(wù)软啼,或者卧檐,多個(gè)線程里執(zhí)行多個(gè)event loop,他們之間還有交互焰宣,我們這里不在介紹。? ?

????單個(gè)event loop跑在單個(gè)線程有個(gè)好處捕仔,只要自己不主動(dòng)await匕积,就會(huì)一直占有主線程,換句話說榜跌,同步函數(shù)一定沒有數(shù)據(jù)沖突(data racing)闪唆。對(duì)比多線程方案,如果需要處理數(shù)據(jù)沖突钓葫,就需要加鎖了悄蕾,這在很多情況下會(huì)降低程序的性能。所以協(xié)程這種設(shè)計(jì)思路础浮,非常適合有多個(gè)用戶帆调、但是每個(gè)用戶之間沒有共享數(shù)據(jù)的場景。如果需要實(shí)現(xiàn)并行豆同,多開幾個(gè)進(jìn)程就行了番刊。

? ? 但是實(shí)際上在工程里面,我們很難單用一個(gè)線程處理問題影锈,asyncio也不例外芹务,特別在集成別的同步庫的時(shí)候蝉绷,可能需要用到別的線程,我們后續(xù)介紹枣抱。


后續(xù)筆記

asyncio常用接口及其意義(實(shí)用主義)

如何集成asyncio和同步庫(介紹executor線程池對(duì)event loop的影響)

為什么異步編程容易犯錯(cuò)(數(shù)據(jù)沖突)


不適宜騷年的附錄

1. Task的構(gòu)造的c實(shí)現(xiàn)

????我們打開男性社交網(wǎng)站熔吗,這是event loop實(shí)現(xiàn)的核心代碼。在這里我們找到了task的c實(shí)現(xiàn)_asyncio_Task___init___impl(L1933)佳晶,它的核心代碼執(zhí)行ask_all_step_soon桅狠,間接調(diào)用腳本的event loop的call_soon,并且把自己加入到all_task這個(gè)全局list(通過register_task宵晚,主要是后面索引使用)垂攘。

圖10 task構(gòu)造函數(shù)實(shí)現(xiàn)

2. task執(zhí)行細(xì)節(jié)

????task的執(zhí)行,實(shí)現(xiàn)在task_step(L2878)和task_step_impl(L2540)? 淤刃。其中task_step是asyncio任務(wù)執(zhí)行的核心晒他,對(duì)于一個(gè)coroutine,每次task_step得到一個(gè)結(jié)果逸贾,然后根據(jù)結(jié)果判斷是否拿到了最終結(jié)果陨仅,或者需要繼續(xù)計(jì)算等待別的結(jié)果,或者把結(jié)果扔給自己的waiter铝侵。

????python的await都是通過generator實(shí)現(xiàn)的灼伤,具體的計(jì)算在genobject,主要是通過PyEval_EvalFrameEx拿計(jì)算結(jié)果咪鲜。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末狐赡,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子疟丙,更是在濱河造成了極大的恐慌颖侄,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件享郊,死亡現(xiàn)場離奇詭異览祖,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)炊琉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門展蒂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人苔咪,你說我怎么就攤上這事锰悼。” “怎么了悼泌?”我有些...
    開封第一講書人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵松捉,是天一觀的道長。 經(jīng)常有香客問我馆里,道長隘世,這世上最難降的妖魔是什么可柿? 我笑而不...
    開封第一講書人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮丙者,結(jié)果婚禮上复斥,老公的妹妹穿的比我還像新娘。我一直安慰自己械媒,他們只是感情好目锭,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著纷捞,像睡著了一般痢虹。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上主儡,一...
    開封第一講書人閱讀 49,166評(píng)論 1 284
  • 那天奖唯,我揣著相機(jī)與錄音,去河邊找鬼糜值。 笑死丰捷,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的寂汇。 我是一名探鬼主播病往,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼骄瓣!你這毒婦竟也來了停巷?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤榕栏,失蹤者是張志新(化名)和其女友劉穎叠穆,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體臼膏,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年示损,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了渗磅。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡检访,死狀恐怖始鱼,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情脆贵,我是刑警寧澤医清,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站卖氨,受9級(jí)特大地震影響会烙,放射性物質(zhì)發(fā)生泄漏负懦。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一柏腻、第九天 我趴在偏房一處隱蔽的房頂上張望纸厉。 院中可真熱鬧,春花似錦五嫂、人聲如沸颗品。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽躯枢。三九已至,卻和暖如春槐臀,著一層夾襖步出監(jiān)牢的瞬間锄蹂,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來泰國打工峰档, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留败匹,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓讥巡,卻偏偏與公主長得像掀亩,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子欢顷,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容