【笔记】A Conceptual Overview of asyncio
读了这两篇(也可以说是一篇),我才正确认识了 Python 的协程。不能直接把 Python async/await 当作 JavaScript async/await一样的东西。
- https://docs.python.org/3/howto/a-conceptual-overview-of-asyncio.html
- https://github.com/anordin95/a-conceptual-overview-of-asyncio/tree/main
在学习 Python 协程之前,我对 JavaScript 协程已经略有了解,在 JavaScript async/await 很直觉直接用就可以,在学习 Python 协程的过程中,我发现我对 JavaScript Promise 也忘了不少,之前写过一个博客记录。
一开始我不熟悉 Python asyncio
Python 的协程用起来需要用asyncio.run(),在 Python 3.6 还没有asyncio.run()。asyncio.create_task() 为什么报错 No running event loop
Python 3.12.3 (main, Jan 8 2026, 11:30:50) [GCC 13.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import asyncio
>>> async def foo(): ...
...
>>> asyncio.create_task(foo())
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib/python3.12/asyncio/tasks.py", line 417, in create_task
loop = events.get_running_loop()
^^^^^^^^^^^^^^^^^^^^^^^^^
RuntimeError: no running event loop
>>>
协程不会立刻执行
调用异步函数,创建了一个协程对象。协程对象内部的逻辑不会立刻执行,等到事件循环拥有控制权,并且启动/继续协程的时候,协程的逻辑才会执行。
Python 3.12.3 (main, Jan 8 2026, 11:30:50) [GCC 13.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import asyncio
>>> async def foo(): print('bar')
...
>>> foo()
<coroutine object foo at 0x720ef6db5a80>
>>>
我们可以看到协程的内部的逻辑没有执行。要执行这个协程我们可以使用 asyncio.run()。
>>> asyncio.run(foo())
bar
在 Python 的协程中,要想启动另外一个协程,就要用 asyncio.create_task() 创建一个 Task 并且和事件循环关联起来,这个函数会找到当前正在运行的事件循环。一个线程只有一个正在运行的事件循环。
asyncio.run() 和 asyncio.create_task() 帮我做了一些事情,但是我不知道他到底干什么了。老实说,我很讨厌编程语言各种隐式的行为。
asyncio.run() 相当于是这样几行:
loop = asyncio.new_event_loop()
task = asyncio.Task(foo(), loop=loop)
loop.run_util_complete(task)
run_util_complete 支持 coroutine,也可以不手动创建 Task.
loop = asyncio.new_event_loop()
loop.run_util_complete(foo())
我们见到了手动创建 Task 的方法,可以想象到 create_task 是如何知道放在哪个事件循环上的。
def create_task(coro):
loop = asyncio.get_running_loop()
return asyncio.Task(coro, loop=loop)
JavaScript 调用异步函数直接就执行了函数体,哪怕你不加 await。JavaScript 异步函数体就相当于传递给 Promise 的回调函数。
let promise = new Promise(function(resolve, reject) {
// this function will executes immediately
})
让步控制权给事件循环
class YieldToEventLoop:
def __await__(self):
yield
The only way to yield (or effectively cede control) from a coroutine is to await an object that
yields in its__await__method.
在一个对象的 __await__ 方法中 yield 是让步控制权给事件循环的唯一的方式。await object 会调用对象的 __awit__ 方法。
await coroutine 不会将控制权交给事件循环,简单的 await coroutine 就好似 yield from generator。这对熟悉 JavaScript 协程的人是很反直觉的。Python 的经典协程,也就是 Python 生成器当作协程使用,但是我们没有事件循环。
coroutine 指的是 coroutine object。调用一个 async function 会创建一个 coroutine object。
await 一个 Task(Future) 对象就会让步控制给事件循环。Task 是 Future 的子类,Future 的 __await__ 方法会 yield。
注意,我们不能直接在异步函数(协程函数)里面直接写 yield 实现让步控制给事件循环,调用函数就会变成异步生成器。
我们需要持有 Task 对象,并且 await
It’s important to be aware that the task itself is not added to the event loop, only a callback to the task is. This matters if the task object you created is garbage collected before it’s called by the event loop
Task 对象一定要有变量持有,并 await。不然当变量超出作用域,被 GC 回收后,协程没执行完自己就静悄悄退出了(不会报错)。Task 对象 GC 回收,那么 coroutine 对象也被回收了。
说实话,我很难理解事件循环没有 Task 对象。但是反过来 JavaScript 的 Promise .then 的时候不就是在注册回调函数吗,V8 的微任务队列里面不是保存 Promise 的对象,是回调函数啊。
import asyncio
async def async_print(str):
print(str)
async def coro_a():
for _ in range(3):
await async_print("I am coro_a()!")
class YieldToEventLoop:
def __await__(self):
yield
async def coro_b():
for _ in range(3):
print("I am coro_b()!")
await YieldToEventLoop() # neccessay to show coro_b() one time
async def main():
task = asyncio.create_task(coro_b())
await coro_a()
asyncio.run(main())
# Output:
# I am coro_a()!
# I am coro_a()!
# I am coro_a()!
# I am coro_b()!
如果读懂了这个例子,那也就读懂了整篇笔记或者原文了。
main() 函数创建 task 后没有 await task,不会让步控制权给事件循环,所以 main 函数继续执行。await coro_a 在 await coroutine,不会让步控制给事件循环,执行 coro_a()。coro_a() await 的也是协程对象,也不会让步给事件循环,所以连着打印了三次I am coro_a()!。循环执行完毕后 main() 协程对象执行完了,事件循环才拿到控制权,开始执行 coro_b 的逻辑。coro_b 刚开始一次,协程 await YieldToEvnetLoop 让步控制给事件循环。因为 main() 函数早已执行完毕退出 task 早已超出作用域,被 GC 回收,等事件循环再次运行 coro_b 的时候发现对应的 Task 对象已经被 GC 回收了。所以就打印了一次 I am coro_b()!。
我很好奇,这个地方创建回调函数的时候怎么就没有形成闭包呢?怎么就没有把 task 对象隐藏在闭包里面呢?
总结
await 协程不会让步控制权,await Future 才会让步控制权给 Event Loop。实现协程让步就是在 __await__ 方法中 yield。
Python 不像 JavaScript 有微任务队列,我们需要自己主动去创建事件循环,并将任务放到事件循环中去,当然更准确的说法是在事件循环中注册回调函数。也就是 asyncio.create_task(), asyncio.run() 这两个函数。
JavaScript 都是 Promise 对象,await Promise 对象就会让步。

浙公网安备 33010602011771号