Python 的异步编程允许程序在等待网络、文件或其他 I/O 操作时暂时切换到别的任务。async def 和 await 是 Python 表达异步控制流的主要语法,asyncio 则提供事件循环、任务、超时和并发组合等基础能力。
异步并发适合包含大量等待的 I/O 密集型程序。它不等同于多个 CPU 核心同时执行计算,也不会自动让同步阻塞代码变成非阻塞代码。
协程函数与协程对象
使用 async def 定义的是协程函数:
async def fetch_name() -> str:
return "Kloudy"
调用协程函数会得到协程对象,但不会像普通函数一样立刻执行完整函数体:
coroutine = fetch_name()
协程对象需要被 await、包装为任务,或者交给事件循环运行。Python 官方的协程与任务文档区分了协程、Task 和 Future 等可等待对象。
await
await 会暂停当前协程,等待另一个可等待对象完成。在等待期间,事件循环可以运行其他已就绪的任务:
import asyncio
async def fetch_name() -> str:
await asyncio.sleep(1)
return "Kloudy"
async def main() -> None:
name = await fetch_name()
print(name)
asyncio.run(main())
await 通常只能出现在 async def 定义的函数中。它适用于明确提供异步接口的操作,例如异步 HTTP 请求、异步数据库调用和 asyncio API。
Task 与并发执行
直接 await 多个协程通常会按代码顺序等待。需要让独立操作并发推进时,可以创建 Task:
async def main() -> None:
first = asyncio.create_task(fetch_name())
second = asyncio.create_task(fetch_name())
first_result = await first
second_result = await second
Task 把协程交给事件循环调度。Python 3.11 及以上版本还可以使用 TaskGroup 管理一组相关任务:
async def main() -> None:
async with asyncio.TaskGroup() as group:
first = group.create_task(fetch_name())
second = group.create_task(fetch_name())
print(first.result(), second.result())
TaskGroup 会等待组内任务结束,并以结构化方式处理任务失败和取消。并发任务仍然共享同一个事件循环线程,除非程序另外使用线程或进程。
阻塞代码的影响
在异步函数中直接调用阻塞操作会阻塞事件循环:
import time
async def blocked() -> None:
time.sleep(5)
这五秒内,同一事件循环中的其他任务也可能无法继续运行。异步代码应优先调用提供 await 接口的库;必须调用同步阻塞函数时,可以根据具体情况把它放到线程中执行。
大量 CPU 计算不会因为使用 async def 而获得并行能力。CPU 密集型工作通常需要进程池、任务队列或独立计算服务。
取消与清理
异步任务可能因为超时、服务关闭或上层请求取消而被取消。资源清理应放在 finally 或异步上下文管理器中:
async def use_resource() -> None:
resource = await open_resource()
try:
await resource.process()
finally:
await resource.close()
不要无条件吞掉取消异常,否则上层可能无法及时停止任务。
与 Web API 的关系
Web 服务经常需要等待客户端、数据库和其他 HTTP 服务,因此适合使用异步并发。FastAPI 的异步说明建议:调用需要 await 的异步库时使用 async def;调用同步阻塞库时可以使用普通 def,由框架按相应方式执行。
选择 async def 还是 def 应由所调用接口的实际性质决定,而不是仅根据函数是否属于 Web API。