玩轉 Python 3.5 的 await/async

發表於2016-09-07

最近通過的PEP-0492為 Python 3.5 在處理協程時增加了一些特殊的語法。新功能中很大一部分在3.5 之前的版本就已經有了,不過之前的語法並不算最好的,因為生成器和協程的概念本身就有點容易混淆。PEP-0492 通過使用 async 關鍵字顯式的對生成器和協程做了區分。

本文旨在說明這些新的機制在底層是如何工作的。如果你只是對怎麼使用這些功能感興趣,那我建議你可以忽略這篇文章,而是去看一下內建的 asyncio 模組的文件。如果你對底層的概念感興趣,關心這些底層功能如何能構建你自己的 asyncio 模組,那你會發現本文會有有意思。

本文中我們會完全放棄任何非同步 I/O 方法,而只限於使用多協程的互動。下面是兩個很小的函式:

我們從兩個最簡單的函式開始,coro1和coro2。我們可以按順序來執行這兩個函式:

我們得到期望的輸出結果:

不過,基於某些原因,我們可能會期望這些程式碼互動執行。普通的函式做不到這點,所以我們把這些函式轉換成攜程:

通過新的 async 關鍵字的魔法,這些函式不再是函式了,現在它們變成了協程(更準確的說是本地協程函式)。普通函式被呼叫的時候,函式體會被執行,但是在呼叫協程函式的時候,函式體並不會被執行,你得到的是一個協程物件:

輸出:

(直譯器還會列印一些執行時的警告資訊,先忽略掉)。

那麼,為什麼要有一個協程物件?程式碼到底如何執行?執行協程的一種方式是使用 await 表示式(使用新的 await 關鍵字)。你可能會想,可以這樣來做:

不過,你肯定會失望了。await 表示式只有在本地協程函式裡才是有效的。你必須這樣做:

接下來問題來了,main 函式又是如何開始執行的呢?

關鍵之處是協程確實是與 Python 的生成器非常相似,也都有一個 send 方法。我們可以通過呼叫 send 方法來啟動一個協程的執行。

這樣我們的第一個協程終於可以執行完成了,不過我們也得到了一個討厭的 StopIteration 異常:

StopIteration 異常是一種標記生成器(或者像這裡的協程)執行結束的機制。雖然這是一個異常,但是確實是我們期望的!我們可以用適當的 try-catch 程式碼將其包起來,這樣就可以避免錯誤提示。接下來我們讓我們的第二個協程也執行起來:

現在我們得到了全部的輸出,不過有點讓人失望的是這跟最初的輸出結果沒有啥區別。因此我們增加了不少程式碼,不過還沒有做到交替執行。協程與執行緒相似的地方是多個執行緒之間也可以交替執行,不過與執行緒不同之處在於協程之間的切換是顯式的,而執行緒是隱式的(大多數情況下是更好的方式)。所以我們需要加入顯式切換的程式碼。

通常生成器的 send 方法會一直執行,直到通過 yield 關鍵字放棄執行,也許你認為我們的 coro1 可以改成這個樣子:

但是我們不能在協程裡使用 yield。作為替換,我們可以使用新的 await 表示式來暫停協程的執行,直到 awaitable 執行結束。於是我們需要的程式碼類似於 await _something_;問題是這裡 _something_ 是什麼呢?我們必須 await 某個東西,而不是空!這個 PEP 解釋了什麼是可以 await 的(awaitable)。其中一種是另一個本地協程,不過這個對我們瞭解底層細節沒有啥幫助。另一種是通過特定 CPython API 定義的物件,不過我們暫時還不打算引入擴充套件模組,而只限於使用純 Python。除此之外,還剩下兩種選擇:基於生成器的協程物件,或者一個特殊的類似 Future 的物件。

接下來,我們會選擇基於生成器的協程物件。基本上一個 Python 的生成器(例如:某個有yield表示式的函式)可以通過 types.coroutine 裝飾被標記成一個協程。所以,這是一個最簡單的例子:

這定義了一個基於生成器的協程函式。要得到基於生成器的協程物件,只需要執行這個函式。我們可以把我們的 coro1 協程修改成下面這樣:

通過上面的修改,我們期望 coro1 和 coro2 可以交錯執行。到目前為止,輸出是這樣的:

我沒看到正如期望的,在第一條列印語句之後,coro1 停止執行,coro2 接著執行。實際上,我們可以通過下面的程式碼檢視協程物件是如何暫停執行的:

這可以列印 await 表示式所在的行。(注意:列印的是最外層的 await,所以這裡只是起示例作用,通常情況下用處不大)。

現在的問題是,如何讓 coro1 繼續執行完呢?我們可以再呼叫一次 send,程式碼如下:

得到的輸出跟預期一樣:

目前,我們通過為不同的協程顯式呼叫 send來讓它們都執行結束。通常情況下這種方式不是很好。我們希望的是有一個函式來控制所有的協程的執行,直到全部協程都執行完成。換句話說,我們期望連續不斷的呼叫 send,驅動不同的協程去執行,直到send丟擲 StopIteration 異常。

為此我們新建一個函式,這個函式傳入一個協程列表,函式執行這些協程直到全部結束。我們現在要做的就是呼叫這個函式。

這段程式碼每次從協程列表裡取一個協程執行,如果捕獲到 StopIteration 異常,就把這個協程從佇列裡去掉。

接下來我們把手工呼叫 send 的程式碼去掉,程式碼如下:

綜上所述,在 Python 3.5,我們現在可以通過新的 await 和 async 功能很輕鬆的執行協程。本文的相關程式碼可以在github 上找到。

相關文章