Python 模塊 asyncio-異步IO,事件循環(huán)和并發(fā)
模塊 asyncio 是一個(gè)異步IO和并發(fā)框架。
asyncio 提供了協(xié)程 coroutines 創(chuàng)建并發(fā)應(yīng)用,它使用單線程,單進(jìn)程的模式進(jìn)行顯示的任務(wù)切換。大部分的任務(wù)切換都發(fā)生在可能會阻塞的地方,例如讀取文件或者網(wǎng)絡(luò)等等。asyncio 提供了一些特性包括在指定時(shí)間運(yùn)行某個(gè)任務(wù),指示某個(gè) coroutines 等待其他的完成才開始執(zhí)行等等。
模塊 threading 和 multiprocessing 分別使用多線程和多進(jìn)程進(jìn)行多任務(wù)的同步運(yùn)行。
概念
大多數(shù)應(yīng)用程序都是線性的開發(fā),然后依賴語言底層的線程或者進(jìn)程切換任務(wù)并行運(yùn)行。基于 asyncio 開發(fā)的并發(fā)程序需要在程序中手動進(jìn)行上下文的切換,因?yàn)樗\(yùn)行在單線程,單進(jìn)程的模式上。下面是需要理解的一些概念。
asyncio 框架里需要重點(diǎn)專注的是事件循環(huán)(event loop),它是處理事件(event)的一個(gè)主要對象,例如IO事件、系統(tǒng)事件、應(yīng)用任務(wù)切換等等。
應(yīng)用首先需要注冊(register)要運(yùn)行的任務(wù)到事件循環(huán)中,當(dāng)?shù)玫剿璧馁Y源后,已注冊的任務(wù)被事件循環(huán)喚醒執(zhí)行。例如服務(wù)端程序當(dāng)收到一個(gè)客戶端的請求或者有數(shù)據(jù)要讀取時(shí)再執(zhí)行操作,當(dāng)處理完成后,立刻把控制權(quán)交回給事件循環(huán)準(zhǔn)備接受下一個(gè)事件。
控制器交回給事件循環(huán)依賴協(xié)程 coroutines,它是一個(gè)特殊的函數(shù)把控制器交回而不丟失狀態(tài),這和 yield 非常類似。事實(shí)上,在 Python 3.5 之前要想實(shí)現(xiàn)協(xié)程,就要使用 yield 生成器函數(shù)。asyncio 提供了基于類的抽象層,可以直接寫回調(diào)方法而不用寫協(xié)程。
對象 Future 是一個(gè)表示結(jié)果的數(shù)據(jù)結(jié)構(gòu),asyncio 可以監(jiān)控一個(gè) Future 對象允許應(yīng)用等待一項(xiàng)任務(wù)完成時(shí)返回。
Future 的子類 Task 知道怎么管理一個(gè)協(xié)程的執(zhí)行,Task 可以等待一個(gè)資源可用時(shí),由事件循環(huán)調(diào)用。
協(xié)程 Coroutine
協(xié)程 Coroutine 是運(yùn)行并發(fā)操作的一個(gè)語言結(jié)構(gòu),一個(gè)協(xié)程函數(shù)調(diào)用的時(shí)候就創(chuàng)建了一個(gè)攜程對象,然后調(diào)用對象的 send() 方法就會執(zhí)行它定義的代碼。協(xié)程還可以使用 await 關(guān)鍵字暫停執(zhí)行,暫停的時(shí)候不會丟失狀態(tài),然后可以等待喚醒繼續(xù)執(zhí)行。
運(yùn)行協(xié)程
要讓一個(gè)事件循環(huán)運(yùn)行協(xié)程,最簡單的方法是調(diào)用 run_until_complete(),參數(shù)傳遞一個(gè)協(xié)程對象。
執(zhí)行:
本例使用 async 關(guān)鍵字放在函數(shù) coroutine() 之前,代表這是一個(gè)協(xié)程函數(shù)。run_until_complete() 方法傳入?yún)f(xié)程對象,開始事件循環(huán),直到協(xié)程對象退出后返回。***使用 try:finally 確保***關(guān)閉事件循環(huán)。
從協(xié)程返回值
run_until_complete() 可以返回協(xié)程的結(jié)果。
執(zhí)行:
協(xié)程鏈
一個(gè)協(xié)程可以啟動另一個(gè)協(xié)程,并等待它的結(jié)果,這樣更容易把一個(gè)任務(wù)分解成多個(gè)可重用的部分。下面的例子展示了必須順序執(zhí)行的兩個(gè)協(xié)程,但是和其他的協(xié)程可以并發(fā)的運(yùn)行。
執(zhí)行:
本例在協(xié)程 worker() 中,創(chuàng)建了兩個(gè)協(xié)程,使用關(guān)鍵字 await。因?yàn)榭刂屏饕呀?jīng)在事件循環(huán)中了,所以這里創(chuàng)建的兩個(gè)協(xié)程也被事件循環(huán)管理。
協(xié)程調(diào)用普通函數(shù)
asyncio 在事件循環(huán)中還可以調(diào)用普通函數(shù),如果對調(diào)用時(shí)間沒有要求,方法 call_soon() 會在事件循環(huán)的下次調(diào)用函數(shù)。
call_soon() 方法的***個(gè)參數(shù)是函數(shù)引用,第二個(gè)參數(shù)是傳遞給函數(shù)的參數(shù)。如果需要傳遞多個(gè)參數(shù),例如關(guān)鍵字參數(shù),可以使用 functools 模塊的 partial() 函數(shù)。
執(zhí)行:
延遲調(diào)用函數(shù)
使用方法 call_later() 延遲調(diào)用回調(diào)函數(shù),***個(gè)參數(shù)是要延遲的時(shí)間,單位是秒。
執(zhí)行:
本例中,同樣的回調(diào)函數(shù)使用不同的參數(shù)調(diào)用了多次,call_soon() 方法會使用最小的延遲時(shí)間,所以它***個(gè)執(zhí)行。
指定的時(shí)間調(diào)用函數(shù)
有時(shí)候需要在指定的時(shí)間執(zhí)行回調(diào)函數(shù)。事件循環(huán)使用的時(shí)鐘是 monotonic clock,而不是掛鐘時(shí)間 wall time。所以為了保證時(shí)間不會倒退,應(yīng)該使用事件循環(huán)的時(shí)間,因?yàn)?wall time 是可以修改的。
monotonic clock 代表某個(gè)時(shí)間點(diǎn)自然流逝的時(shí)間,不受 time-of-day 時(shí)鐘修改的影響,例如你不想因?yàn)殡娔X重啟而影響時(shí)間的話,就應(yīng)該使用它。
wall time 通常就是我們在電腦上看到的時(shí)間,可以手動修改也包括 NTP 對它的修改。(NTP: Network Time Protocol 是用來使網(wǎng)絡(luò)時(shí)間和本地時(shí)間同步的協(xié)議,它可以使服務(wù)器或時(shí)鐘源同步修改時(shí)間)
執(zhí)行:
