我们在使用Python时大概率写过 async def 和 await,也知道表面怎么用。可能也遇到过:死锁、性能异常、不符合预期的执行顺序。这些问题的根源,是对AsyncIO理解不够清晰。在AsyncIO之前,主流的异步模型只有两种,1.线程,有并发效果,但是有共享状态、锁竞争、复杂度高。2.回调,非阻塞IO,但是代码嵌套,可读性差。协程是第三种方案,一个可以在特定位置暂停执行,并从暂停状态精准恢复的函数。async def发生了什么呢?当调用async def函数,并不会执行函数体,它会返回一个协程对象async def fetch(url): print(f"fetching {url}") return "data"# 这里只是创建对象,函数内部根本没执行!coro = fetch("https://example.com")print(type(coro)) # <class 'coroutine'>
协程对象的本质是实现了三个底层方法,send()、throw()、close(),时间循环就是靠不断调用send(None)驱动协程运行,甚至可以手动驱动协程:async def simple(): return 42coro = simple()try: coro.send(None) # 启动协程except StopIteration as e: print(e.value) # 42
这就是事件循环做的事情,只是外面包了一层调度逻辑。那么await到底干了什么?它接受一个可等待的对象,然后暂停当前协程,把执行权交给事件循环,等待被等待对象完成,恢复协程,返回结果。接下来我们看看事件循环是什么?它就是一个死循环,只做三件事,1运行所有准备好的回调,2轮询IO是否完成,3把完成的IO加入回调队列。伪代码如下:while True: # 1. 运行所有就绪回调 run_ready_callbacks() # 2. 等待 IO/定时器 timeout = compute_timeout() events = selector.select(timeout) # 3. 处理完成的 IO,调度回调 process_events(events)
asyncio.sleep(1)底层就是:创建一个Future,告诉事件循环1秒后唤醒,暂停当前协程,时间到恢复协程。全程不创建线程,完全是单线程。还有个非常容易混淆的概念,Coroutine/Future/Task,首先Coroutine协程,async def 返回的对象,自己不会跑,必须被驱动;Future底层对象,代表将来会有结果的值,有pending/callelled/done三种状态,真正让协程暂停的就是它。Task 是Future的字类,包装了协程,并自动驱动它运行,asyncio.create_task()会立刻把协程交给事件循环。# 只是协程,不会并发await coro1()await coro2()# 创建 Task → 真正并发运行task1 = asyncio.create_task(coro1())task2 = asyncio.create_task(coro2())await task1await task2
需要注意的是,AsyncIO是单线程协作式,任何不主动暂停的代码,都会直接卡死整个程序,举个例子就是:import timeasync def bad(): time.sleep(2) # 卡死整个事件循环 2 秒!
正确做法是 await asyncio.sleep(2)任何的对象只要实现__await__方法,就能被await,这是aiohttp、asyncpg等底层接入事件循环的方式:class SleepUntilNextSecond: def __await__(self): loop = asyncio.get_event_loop() future = loop.create_future() delay = 1.0 - (time.time() % 1.0) loop.call_later(delay, future.set_result, None) yield from future.__await__() return "woke up"# 使用await SleepUntilNextSecond()
最后再看看任务取消,task.cancel()不会立刻杀死任务。它只是在协程下次await时,跑出CancelledError,如果协程捕获了异常但不重新抛出,取消会失效,正确写法:async def careful_work(): try: await asyncio.sleep(10) except asyncio.CancelledError: # 清理资源 raise # 必须重新抛出,否则取消失败
到此,不知道你对它是否有了一定了解,如果还是不清晰,不妨自己敲敲代码来加深理解。