协程与 asyncio
本教程共 70 篇 · 第 63 篇 · 更新于 2026-07-22 · 约 5 分钟阅读
63. 协程与 asyncio
本节目标:理解协程和事件循环的概念,掌握
async/await语法,能用asyncio并发执行多个 IO 任务。
前面的章节里,多线程和多进程都能实现并发,但都有各自的局限。协程(Coroutine)提供了一种更轻量的方案:单线程内管理多个任务,遇到 IO 等待就切换,不浪费 CPU 时间。
从生成器到协程
Python 3.4 之前,协程靠生成器实现。yield 不仅能返回值,还能接收外部发送的数据:
def consumer():
r = ''
while True:
n = yield r
if n is None:
return
print(f"消费了 {n}")
r = 'done'
def produce(c):
c.send(None)
for n in range(1, 4):
print(f"生产了 {n}")
r = c.send(n)
print(f"消费者返回: {r}")
c.close()
c = consumer()
produce(c)
这个模式叫「生产者-消费者模型」,通过 yield 实现双向通信。但生成器语法晦涩,容易写错。Python 3.5 引入 async/await 后,协程终于有了原生、直观的语法支持。
async 和 await
async def 定义一个协程函数,调用它不会立刻执行,而是返回一个协程对象:
import asyncio
async def hello():
print("Hello")
await asyncio.sleep(1)
print("World")
# 这不是普通函数,返回的是协程对象
coro = hello()
print(type(coro)) # <class 'coroutine'>
# 需要用事件循环来运行
asyncio.run(coro)
关键点:
await后面只能跟「可等待对象」:另一个协程、asyncio.sleep()、asyncio.Future等。await会挂起当前协程,把控制权交还给事件循环,去调度其他任务。asyncio.run()创建事件循环、运行协程、最后关闭循环,是启动 asyncio 程序的推荐入口。
Warning在普通函数里不能用
await,在async def函数里不能用time.sleep()代替asyncio.sleep(),否则整个事件循环会被阻塞。
并发运行多个任务
事件循环的真正威力在于同时管理多个协程。asyncio.gather() 可以并发执行多个任务:
import asyncio
async def fetch(url, delay):
print(f"开始获取 {url}")
await asyncio.sleep(delay)
print(f"完成获取 {url}")
return f"{url} 的数据"
async def main():
tasks = [
fetch('url-a', 2),
fetch('url-b', 1),
fetch('url-c', 3)
]
results = await asyncio.gather(*tasks)
print(results)
asyncio.run(main())
三个任务几乎同时启动,url-b 最先完成,url-a 次之,url-c 最后。总耗时取决于最慢的那个任务,约 3 秒,而不是顺序执行的 6 秒。
如果想按完成顺序处理结果,用 as_completed:
import asyncio
async def main():
tasks = [fetch('url-a', 2), fetch('url-b', 1), fetch('url-c', 3)]
for coro in asyncio.as_completed(tasks):
result = await coro
print(f"先收到: {result}")
asyncio.run(main())
创建任务对象
asyncio.create_task() 把协程包装成 Task 对象,立即注册到事件循环中执行,不用等到 await:
async def main():
task1 = asyncio.create_task(fetch('url-a', 2))
task2 = asyncio.create_task(fetch('url-b', 1))
print("任务已提交,我先干点别的")
await asyncio.sleep(0.5)
result1 = await task1
result2 = await task2
print(result1, result2)
asyncio.run(main())
create_task() 适合「先启动、后等待」的场景,比如同时发起多个网络请求,等全部完成后再统一处理。
TipPython 3.11+ 推荐使用
asyncio.TaskGroup,可以用上下文管理器更优雅地管理多个任务:async with asyncio.TaskGroup() as tg: task1 = tg.create_task(fetch('url-a', 2)) task2 = tg.create_task(fetch('url-b', 1)) print(task1.result(), task2.result())
超时控制
网络请求可能卡住,asyncio.wait_for() 可以设置超时:
async def slow():
await asyncio.sleep(10)
return 'done'
try:
result = await asyncio.wait_for(slow(), timeout=2)
except asyncio.TimeoutError:
print("超时了")
事件循环的本质
事件循环是 asyncio 的核心。你可以把它想象成一个勤劳的调度员:
- 维护一个任务队列。
- 从队列里取出一个就绪的任务执行。
- 遇到
await就挂起这个任务,去执行别的任务。 - 等 IO 就绪后,唤醒对应的任务继续执行。
整个过程都在一个线程里完成,没有线程切换的开销,也不需要考虑锁的问题。代价是:一旦某个协程里写了同步阻塞代码(比如 time.sleep() 或大量计算),整个循环都会停顿。
小结
async def定义协程,await挂起并交出控制权。asyncio.gather()并发运行多个任务,as_completed()按完成顺序迭代。asyncio.create_task()立即把协程提交到事件循环。- 事件循环单线程调度,避免阻塞是关键。
来源:参考了 liaoxuefeng「23.1. 协程/23.2. 使用asyncio」、runoob「Python asyncio 模块」等,改写后所得。