首页 / Python3 入门教程 / 协程与 asyncio

Python3 入门教程

协程与 asyncio

本教程共 70 篇 · 第 63 篇 · 更新于 2026-07-22 · 约 5 分钟阅读

PythonPython3 入门教程协程asyncioasync/await

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() 适合「先启动、后等待」的场景,比如同时发起多个网络请求,等全部完成后再统一处理。

Tip

Python 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 的核心。你可以把它想象成一个勤劳的调度员:

  1. 维护一个任务队列。
  2. 从队列里取出一个就绪的任务执行。
  3. 遇到 await 就挂起这个任务,去执行别的任务。
  4. 等 IO 就绪后,唤醒对应的任务继续执行。

整个过程都在一个线程里完成,没有线程切换的开销,也不需要考虑锁的问题。代价是:一旦某个协程里写了同步阻塞代码(比如 time.sleep() 或大量计算),整个循环都会停顿。

小结

  • async def 定义协程,await 挂起并交出控制权。
  • asyncio.gather() 并发运行多个任务,as_completed() 按完成顺序迭代。
  • asyncio.create_task() 立即把协程提交到事件循环。
  • 事件循环单线程调度,避免阻塞是关键。

来源:参考了 liaoxuefeng「23.1. 协程/23.2. 使用asyncio」、runoob「Python asyncio 模块」等,改写后所得。