Python 从 3.5 版本开始引入 async 和 await 关键字,标志着原生协程和异步编程成为语言的一等公民。理解这两个关键字,是进入 Python 异步世界的钥匙。
异步编程解决了什么问题?
在传统的同步 IO 中,当一个任务需要等待网络响应或磁盘读取时,整个线程会被阻塞,什么也做不了。异步编程的核心思想是:在等待 IO 的时间里,不闲着,主动交出控制权去执行其他任务。这样,一个线程就能同时处理成百上千个 IO 密集型任务,极大提高吞吐量。
async def:定义一个协程函数
用 async def 定义的函数不再是普通函数,而是协程函数。调用它不会立即执行内部的代码,而是返回一个协程对象,需要被事件循环调度才能运行。
# 定义一个协程函数
async def fetch_data():
print("开始请求数据...")
await asyncio.sleep(2) # 模拟网络 IO 等待
print("数据请求完成")
return {"status": "ok"}
直接调用 fetch_data() 只会得到一个协程对象,不会打印任何内容。
await:等待一个可等待对象
await 关键字用于挂起当前协程,等待另一个协程(或可等待对象)执行完毕,得到结果后再继续往下走。在挂起期间,事件循环可以转而执行其他任务。
async def main():
result = await fetch_data() # 等待 fetch_data 完成,拿到返回值
print(result)
可等待对象只有三种:协程、asyncio.Task、asyncio.Future。你不能 await 一个普通函数或阻塞操作(如 time.sleep),必须使用异步库提供的对应版本(如 asyncio.sleep)。
事件循环:调度者
要想真正运行协程,必须有一个事件循环。事件循环会不停地检查哪些协程可以继续执行,切换调度。在高级 API 中,asyncio.run() 会自动创建并管理事件循环。
import asyncio
asyncio.run(main()) # 这就是启动整个异步程序的入口
并发执行多个任务:asyncio.gather 与 asyncio.create_task
如果只是顺序地 await 一个接一个的协程,那和同步代码没有区别。真正的并发来自同时启动多个任务。
async def task1():
await asyncio.sleep(1)
return "任务1完成"
async def task2():
await asyncio.sleep(2)
return "任务2完成"
async def main():
# 并发执行 task1 和 task2
results = await asyncio.gather(task1(), task2())
print(results) # ['任务1完成', '任务2完成']
asyncio.gather 会并发运行多个协程,直到全部完成。另一种方式是 asyncio.create_task,它将协程封装成 Task 并立即提交给事件循环调度。
异步编程范式:协作式多任务
异步编程是一种协作式而非抢占式的多任务模型。一个协程必须主动通过 await 交出控制权,别的协程才有机会运行。这带来了几个关键特性:
- 非阻塞:所有可能导致阻塞的 IO 操作都必须使用异步版本,否则会卡住整个事件循环。
- 单线程:默认情况下,
asyncio在单个线程里工作,因此没有多线程常见的竞态、锁等复杂问题。 - 适合 IO 密集型:对于大量 IO 等待的任务(如爬虫、Web API、数据库查询),异步性能远超多线程。
- 不适合 CPU 密集型:如果一个协程里包含大量计算,它不主动交出控制权,其他任务就会“饿死”。CPU 密集型任务应交给多进程或线程池。
实际开发中的注意事项
- 切换要彻底:整个调用链都必须使用
async/await,不能中途混入同步阻塞调用。例如,用requests库会卡死事件循环,必须改用aiohttp等异步库。 - 传统同步函数怎么调用:
asyncio.to_thread(Python 3.9+)可以把同步函数放入线程池执行,避免阻塞事件循环。 - 调试与测试:
pytest-asyncio可以让测试函数也变成协程,方便编写异步测试用例。
异步编程上手稍有门槛,但一旦理解“事件循环 + 协程挂起”的模型,就能写出高性能、低资源的并发程序。它已经成为 FastAPI、异步爬虫、实时通信等场景的标准实践。