前段時間使用 asyncio 寫了一個小程序,摸索出一些使用上的注意事項,記錄如下。
本質
有人把 asyncio 這類東西叫做使用同步語法的異步程序,即說明它仍然是異步程序的本質,只不過在語法層面進行了優化,避免陷入回調地獄。取代回調的是 async
、await
關鍵字和coroutine
、Future
對象。
Future
的意思是未來,即尚未發生之事。coroutine
算是一種自主調度的程序執行模式。對標到回調上,我理解的 Future 就像是回調地獄裏的葉子節點,coroutine 算是非葉子節點。因爲 coroutine 可以等待 Future,也可以等待其他 coroutine。
await
即 asynchronous wait,是等待事件發生的意思。async
關鍵字不單獨使用,而是作爲一個副詞修飾其他關鍵字,比如async def
定義一個 coroutine,async for
用於異步迭代,async with
用於異步進入上下文。
coroutine 和生成器之間有着千絲萬縷的聯繫。因爲 Python 中原生能夠實現調用後不立即執行功能的就是生成器,所以早期的三方 coroutine 都是基於生成器做的。通過 yield 進行調出,再使用 .send() 調入。Python3.5 以後有了原生的定義和執行 coroutine 的關鍵字就是 async def 和 await。但基於生成器的語法也被保留,即 @asyncio.coroutine
和 yield from
,不過不允許混用。如果有條件使用 3.5 或更新的版本,建議儘量使用新的關鍵字,因爲更加清晰。
由上可見,多層嵌套的回調執行模式本質上仍然保留,只不過不再出現在源代碼上,因此便稱不上“地獄”了。
time()
當需要使用時間戳的時候,比如 call_at()
,需要注意,loop 有一個自己的 loop.time()
。他的時間比標準時間戳要小,且會迴繞。當用在跟 loop 相關的時間控制時一定要用 loop.time() ,time.time() 只應該用在與 loop 無關的地方,比如業務層或者寫日誌之類的。
timeout waiter
asyncio 自帶的一個超時控制器,不必自己寫了,添加註釋後的源碼如下:
@coroutine
def wait_for(fut, timeout, *, loop=None):
"""Wait for the single Future or coroutine to complete, with timeout.
Coroutine will be wrapped in Task.
Returns result of the Future or coroutine. When a timeout occurs,
it cancels the task and raises TimeoutError. To avoid the task
cancellation, wrap it in shield().
If the wait is cancelled, the task is also cancelled.
This function is a coroutine.
"""
if loop is None:
loop = events.get_event_loop()
if timeout is None:
return (yield from fut)
waiter = loop.create_future()
timeout_handle = loop.call_later(timeout, _release_waiter, waiter) # finish waiter
cb = functools.partial(_release_waiter, waiter)
fut = ensure_future(fut, loop=loop)
fut.add_done_callback(cb) # 若按時完成,則 finish waiter
try:
# wait until the future completes or the timeout
try:
yield from waiter # 等待 waiter finished
except futures.CancelledError: # wait_for 被 cancel
fut.remove_done_callback(cb)
fut.cancel()
raise
if fut.done(): # 若按時完成
return fut.result()
else: # 超時
fut.remove_done_callback(cb)
fut.cancel()
raise futures.TimeoutError()
finally:
timeout_handle.cancel()
run_forever()
run_forever()
不接受參數,如果你想跑一個服務類的進程,需要先使用 ensure_future()
之類的添加一個起始 coroutine,並且保證總是有的跑,不會退出。
cancel()
當 cancel()
一個 coroutine 時,它正在 await 的 coroutine 也會被 cancel。