1. 程式人生 > >本篇為你深度解析 Python 異步到底是怎麽實現的?

本篇為你深度解析 Python 異步到底是怎麽實現的?

mark 基類 技術 ofo 傳播 實現 cdc 3.5 function

技術分享圖片
簡述

PEP492 引入了對 Python 3.5 的原生協程和 async/await 句法的支持。本次提案添加了對異步生成器的支持進而來擴展 Python 的異步功能。

理論和目標

常規生成器(在 PEP 255 中引入)的實現,使得編寫復雜數據變得更優雅,它們的行為類似於叠代器。

當時沒有提供async for使用的異步生成器。 編寫異步數據生成器變得非常復雜,因為必須定義一個實現 aiter 和 anext 的方法,才能在 async for 語句中使用它。

為了說明異步生成器的重要性,專門做了性能測試,測試結果表明使用異步生成器要比使用異步叠代器快 2 倍多。

下面的代碼是演示了在叠代的過程中等待幾秒:

技術分享圖片
我們那可以使用下面的代碼實現同樣的功能:
技術分享圖片
詳細說明

異步生成器

我們直到在函數中使用一個或多個 yield 該函數將變成一個生成器。
技術分享圖片
我們提議使用類似的功能實現下面異步生成器:
技術分享圖片
調用異步生成器函數的結果是異步生成器對象,它實現了 PEP 492 中定義的異步叠代協議。

註意:在異步生成器中使用非空 return 語句會引發 SyntaxError 錯誤。

對異步叠代協議的支持

該協議需要實現兩種特殊方法:

aiter 方法返回一個異步叠代器。

anext 方法返回一個 awaitable 對象,它使用 StopIteration 異常來捕獲 yield 的值,使用 StopAsyncIteration 異常來表示叠代結束。

異步生成器定義了這兩種方法。 讓我們實現一個一個簡單的異步生成器:
技術分享圖片
終止

PEP 492 提到需要使用事件循環或調度程序來運行協程。 因為異步生成器是在協程使用的,所以還需要創建一個事件循環來運行。

異步生成器可以有 try..finally 塊,也可以用 async with 異步上下文管理代碼快。 重要的是提供一種保證,即使在部分叠代時,也可以進行垃圾收集,生成器可以安全終止。
技術分享圖片
上面代碼演示了異步生成器在 async with中 使用,然後使用 async for 對異步生成器對象進行叠代處理,同時我們也可以設置一個中斷條件。

square_series() 生成器將被垃圾收集,並沒有異步關閉生成器的機制,Python 解釋器將無法執行任何操作。

為了解決這個問題,這裏提出以下改進建議:

1.在異步生成器上實現一個 aclose 方法,返回一個特殊 awaittable 對象。 當 awaitable 拋出 GeneratorExit 異常的時候,拋出到掛起的生成器中並對其進行叠代,直到發生 GeneratorExit 或 StopAsyncIteration。這就是在常規函數中使用 close 方法關閉對象一樣,只不過 aclose 需要一個事件循環去執行。

2.不要在異步生成器中使用 yield 語句,只能用 await。

3.在sys模塊中加兩個方法:set_asyncgen_hooks() and get_asyncgen_hooks().

sys.set_asyncgen_hooks() 背後的思想是允許事件循環攔截異步生成器的叠代和終結,這樣最終用戶就不需要關心終結問題了,一切正常。

sys.set_asyncgen_hooks() 可以結束兩個參數

firstiter:一個可調用的,當第一次叠代異步生成器時將調用它。

finalizer:一個可調用的,當異步生成器即將被 GC 時將被調用。

當第一叠代異步生成器時,它會引用到當前的 finalizer。

當異步生成器即將被垃圾收集時,它會調用其緩存的 finalizer。假想在事件循環激活異步生成器開始叠代的時候, finalizer 將調用一個 aclose() 方法.

例如,以下是如何修改 asyncio 以允許安全地完成異步生成器:
技術分享圖片
第二個參數 firstiter,允許事件循環維護在其控制下實例化的弱異步生成器集。這使得可以實現“shutdown”機制,來安全地打開的生成器並關閉事件循環。

sys.set_asyncgen_hooks() 是特定線程,因此在多個事件循環並行的時候是安全的。

sys.get_asyncgen_hooks() 返回一個帶有 firstiter 和 finalizer 字段的類似於類的結構。

asyncio

asyncio 事件循環將使用 sys.set_asyncgen_hooks() API 來維護所有被調度的弱異步生成器,並在生成器被垃圾回收時侯調度它們的 aclose() 方法。

為了確保 asyncio 程序可以可靠地完成所有被調度的異步生成器,我們建議添加一個新的事件循環協程方法 loop.shutdown_asyncgens()。 該方法將使用 aclose() 調用關閉所有當前打開的異步生成器。

在調用loop.shutdown_asyncgens() 方法之後,首次叠代新的異步生成器,事件循環就會發出警告。 我們的想法是,在請求關閉所有異步生成器之後,程序不應該執行叠代新異步生成器的代碼。

下面是一個關於如何使用 Ashutdown_asyncgens 的例子:
技術分享圖片
異步生成器對象

該對象以標準 Python 生成器對象為模型。 本質上異步生成器的行為復制了同步生成器的行為,唯一的區別在於 API 是異步的。

定義了以下方法和屬性:

1.agen.aiter(): 返回 agen.

2.agen.anext(): 返回一個 awaitable 對象, 調用一次異步生成器的元素。

3.agen.asend(val): 返回一個 awaitable 對象,它在 agen 生成器中推送 val對象。 當 agen 還沒叠代時,val 必須為 None。

上面的方法類似同步生成器的使用。

代碼例子:
技術分享圖片
4.agen.athrow(typ, [val, [tb]]): 返回一個 awaitable 對象, 這會向 agen 生成器拋出一個異常。

代碼如下:
技術分享圖片
5.agen.aclose(): 返回一個 awaitable 對象, 調用該方法會拋出一個異常給生成器。

技術分享圖片
6.agen.name and agen.qualname:可以返回異步生成器函數的名字。
技術分享圖片
其他的方法

agen.ag_await: 正等待的對象(None). 類似當前可用的 gi_yieldfrom for generators and cr_await for coroutines.
agen.ag_frame, agen.ag_running, and agen.ag_code: 同生成器一樣
StopIteration and StopAsyncIteration 被替換為 RuntimeError,並且不上拋。

源碼實現細節

異步生成器對象(PyAsyncGenObject)與 PyGenObject 共享結構布局。 除此之外,參考實現還引入了三個新對象:

PyAsyncGenASend:實現 anext 和 asend() 方法的等待對象。

PyAsyncGenAThrow:實現 athrow() 和 aclose() 方法的等待對象。

PyAsyncGenWrappedValue:來自異步生成器的每個直接生成的對象都隱式地裝入此結構中。 這就是生成器實現如何使用常規叠代協議從使用異步叠代協議生成的對象中分離出的對象。 PyAsyncGenASend和 PyAsyncGenAThrow 是 awaitable 對象(它們有 await 方法返回 self)類似於 coroutine 的對象(實現iter, next,send() 和 throw() 方法)。 本質上,它們控制異步生成器的叠代方式。

PyAsyncGenASend and PyAsyncGenAThrow

PyAsyncGenASend 類似生成器對象驅動 anext and asend() 方法,實裝了異步叠代協議。

agen.asend(val) 和 agen.anext() 返回一個 PyAsyncGenASend 對象的一個引用。 (它將引用保存回父類 agen 對象。)

數據流定義如下:

1.首次調用 PyAsyncGenASend.send(val) 時, val將 推入到父類 agen 對象 (PyGenObject 利用現有對象。)

對 PyAsyncGenASend 對象進行後續叠代,將 None 推送到 agen。

2.首次調用 _PyAsyncGenWrappedValue 對象時,它將被拆箱,並且以未被裝飾的值作為參數會引發 StopIteration 異常。

3.異步生成器中的 return 語句引發 StopAsyncIteration 異常,該異常通過 PyAsyncGenASend.send() 和 PyAsyncGenASend.throw() 方法傳播。

4.PyAsyncGenAThrow與PyAsyncGenASend非常相似。 唯一的區別是PyAsyncGenAThrow.send() 在第一次調用時會向父類 agen 對象拋出異常(而不是將值推入其中。)

新的標準庫方法和Types

1.types.AsyncGeneratorType -- 判斷是否是異步生成器對象

2.sys.set_asyncgen_hooks() 和 sys.get_asyncgen_hooks()--

在事件循環中設置異步生成器終結器和叠代攔截器。

3.inspect.isasyncgen() 和 inspect.isasyncgenfunction() :方法內省。

4.asyncio 加入新方法: loop.shutdown_asyncgens().

5.collections.abc.AsyncGenerator: 抽象基類的添加。

是否支持向後兼容

該提案完全支持向後兼容

在 python3.5,async def 裏使用 yield 會報錯,因此在 python3.6 引入了安全的異步生成器

性能展示

常規生成器

技術分享圖片
輸出

技術分享圖片
15s 左右

異步叠代器的改進

技術分享圖片
輸出

技術分享圖片
很明顯叠代異步生成器的速度比叠代普通生成器不只是快了兩倍。

我們可以做一個更簡單的異步生成器
技術分享圖片
設計中要註意的事項

內建函數: aiter() and anext()

最初,PEP 492 將 aiter 定義為應返回等待對象的方法,從而產生異步叠代器。

但是,在CPython 3.5.2中,重新定義了 aiter 可以直接返回異步叠代器。

為了避免破壞向後兼容性,決定 Python 3.6 將支持兩種方式:aiter 仍然可以在發出 DeprecationWarning 時返回等待狀態。由於 Python 3.6 中 aiter 的這種雙重性質,我們無法添加內置的 aiter() 的同步實現。 因此,建議等到 Python 3.7。

異步list/dict/set 推導式

將放在單獨的 pep 中也就是後來的 pep530.

異步 yield from

對於異步生成器,yield from 也不那麽重要,因為不需要提供在協程之上實現另一個協同程序協議的機制。為了組合異步生成器,可以使用 async for簡化這個過程:
技術分享圖片
為了 asend() 和 athrow() 是必須的

它們可以使用異步生成器實現類似於 contextlib.contextmanager 的概念。 例如,可以實現以下模式:
技術分享圖片
另一個原因是從 anext 對象返回的對象來推送數據並將異常拋出到異步生成器中,很難正確地執行此操作。 添加顯式的asend()和athrow()更獲取異常後的數據。

在實現方面,asend() 是 anext 更通用的版本,而 athrow() 與 aclose() 非常相似。 因此,為異步生成器定義這些方法不會增加任何額外的復雜性。

代碼示例
技術分享圖片
這代碼將打出 0-9,每個數字之間的間隔為 1s。

提議者

Guido, 2016 年9 月 6 日

參考資料

[1] https://github.com/1st1/cpython/tree/async_gen
[2] https://mail.python.org/pipermail/python-dev/2016-September/146267.html
[3] http://bugs.python.org/issue28003
最後,如果你跟我一樣都喜歡python,想成為一名優秀的程序員,也在學習python的道路上奔跑,歡迎你加入python學習群:839383765 群內每天都會分享最新業內資料,分享python免費課程,共同交流學習,讓學習變(編)成(程)一種習慣!

本篇為你深度解析 Python 異步到底是怎麽實現的?