Python 模組 asyncio-非同步IO,事件迴圈和併發
模組 asyncio 是一個非同步IO和併發框架。
asyncio 提供了協程 coroutines 建立併發應用,它使用單執行緒,單程序的模式進行顯示的任務切換。大部分的任務切換都發生在可能會阻塞的地方,例如讀取檔案或者網路等等。asyncio 提供了一些特性包括在指定時間執行某個任務,指示某個 coroutines 等待其他的完成才開始執行等等。
模組 threading 和 multiprocessing 分別使用多執行緒和多程序進行多工的同步執行。
概念
大多數應用程式都是線性的開發,然後依賴語言底層的執行緒或者程序切換任務並行執行。基於 asyncio 開發的併發程式需要在程式中手動進行上下文的切換,因為它執行在單執行緒,單程序的模式上。下面是需要理解的一些概念。
asyncio 框架裡需要重點專注的是事件迴圈(event loop),它是處理事件(event)的一個主要物件,例如IO事件、系統事件、應用任務切換等等。
應用首先需要註冊(register)要執行的任務到事件迴圈中,當得到所需的資源後,已註冊的任務被事件迴圈喚醒執行。例如服務端程式當收到一個客戶端的請求或者有資料要讀取時再執行操作,當處理完成後,立刻把控制權交回給事件迴圈準備接受下一個事件。
控制器交回給事件迴圈依賴協程 coroutines,它是一個特殊的函式把控制器交回而不丟失狀態,這和 yield 非常類似。事實上,在 Python 3.5 之前要想實現協程,就要使用 yield 生成器函式。asyncio 提供了基於類的抽象層,可以直接寫回調方法而不用寫協程。
物件 Future 是一個表示結果的資料結構,asyncio 可以監控一個 Future 物件允許應用等待一項任務完成時返回。
Future 的子類 Task 知道怎麼管理一個協程的執行,Task 可以等待一個資源可用時,由事件迴圈呼叫。
協程 Coroutine
協程 Coroutine 是執行併發操作的一個語言結構,一個協程函式呼叫的時候就建立了一個攜程物件,然後呼叫物件的 send() 方法就會執行它定義的程式碼。協程還可以使用 await 關鍵字暫停執行,暫停的時候不會丟失狀態,然後可以等待喚醒繼續執行。
執行協程
要讓一個事件迴圈執行協程,最簡單的方法是呼叫 run_until_complete(),引數傳遞一個協程物件。
執行:
本例使用 async 關鍵字放在函式 coroutine() 之前,代表這是一個協程函式。run_until_complete() 方法傳入協程物件,開始事件迴圈,直到協程物件退出後返回。最後使用 try:finally 確保最後關閉事件迴圈。
從協程返回值
run_until_complete() 可以返回協程的結果。
執行:
協程鏈
一個協程可以啟動另一個協程,並等待它的結果,這樣更容易把一個任務分解成多個可重用的部分。下面的例子展示了必須順序執行的兩個協程,但是和其他的協程可以併發的執行。
執行:
本例在協程 worker() 中,建立了兩個協程,使用關鍵字 await。因為控制流已經在事件迴圈中了,所以這裡建立的兩個協程也被事件迴圈管理。
協程呼叫普通函式
asyncio 在事件迴圈中還可以呼叫普通函式,如果對呼叫時間沒有要求,方法 call_soon() 會在事件迴圈的下次呼叫函式。
call_soon() 方法的第一個引數是函式引用,第二個引數是傳遞給函式的引數。如果需要傳遞多個引數,例如關鍵字引數,可以使用 functools 模組的 partial() 函式。
執行:
延遲呼叫函式
使用方法 call_later() 延遲呼叫回撥函式,第一個引數是要延遲的時間,單位是秒。
執行:
本例中,同樣的回撥函式使用不同的引數呼叫了多次,call_soon() 方法會使用最小的延遲時間,所以它第一個執行。
指定的時間呼叫函式
有時候需要在指定的時間執行回撥函式。事件迴圈使用的時鐘是 monotonic clock,而不是掛鐘時間 wall time。所以為了保證時間不會倒退,應該使用事件迴圈的時間,因為 wall time 是可以修改的。
monotonic clock 代表某個時間點自然流逝的時間,不受 time-of-day 時鐘修改的影響,例如你不想因為電腦重啟而影響時間的話,就應該使用它。
wall time 通常就是我們在電腦上看到的時間,可以手動修改也包括 NTP 對它的修改。(NTP: Network Time Protocol 是用來使網路時間和本地時間同步的協議,它可以使伺服器或時鐘源同步修改時間)
執行:
