前段时间使用 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。