Python的asyncio库自3.4版本起成为标准库的一部分,在3.7版本中达到稳定API,现在是Python异步生态系统的基础。尽管被广泛使用,asyncio也经常被误用——开发人员编写的异步代码阻塞事件循环、失去并发性,或者增加开销而没有任何好处。本指南专注于重要的模式和要避免的错误。
基础知识
事件循环模型:asyncio使用运行协程的单线程事件循环。关键见解:协程只在遇到`await`表达式时才将控制权让给事件循环。在`await`点之间,执行是同步的,会阻塞其他一切。这是协作多任务——而非抢占式线程。`async def`和`await`的含义:`async def`定义一个协程函数——调用它返回一个协程对象,并不执行。协程在你`await`它时执行(在另一个协程内)或用`asyncio.run()`或`asyncio.create_task()`调度它。`await`暂停当前协程并将控制权让给事件循环,在等待时可以运行其他协程。关键区别:I/O密集型与CPU密集型工作。asyncio擅长I/O密集型并发——同时发出100个HTTP请求、读取50个文件、查询10个数据库连接。对于CPU密集型工作(计算、数据处理、图像编码),它没有任何用处——对于这些,使用`multiprocessing`或`concurrent.futures.ProcessPoolExecutor`。并发运行多个协程:最重要的模式——`asyncio.gather()`接受多个协程并并发运行它们。没有gather(用`await`顺序运行它们),你得不到任何并发好处:
“`python
# 顺序(I/O坏)— 总时间 = 所有时间之和
result1 = await fetch_user(1)
result2 = await fetch_user(2)
# 并发(I/O正确)— 总时间 = 单个时间的最大值
result1, result2 = await asyncio.gather(fetch_user(1), fetch_user(2))
“`
`asyncio.create_task()`:从协程创建任务——任务立即开始运行(事件循环一有控制权)。当你需要启动一个协程并在它运行时做其他事情,然后稍后收集结果时使用这个。`asyncio.TaskGroup`(Python 3.11+):用结构化并发语义管理任务组的首选方式——如果一个任务引发异常,组中的所有任务都会被取消。
常见错误及如何避免
阻塞事件循环:从协程中调用任何阻塞操作会阻塞整个事件循环——所有其他协程冻结直到它返回。常见罪魁祸首:`time.sleep()`(使用`await asyncio.sleep()`)、`requests.get()`(使用`httpx`或`aiohttp`)、`open()`和文件读取(使用`aiofiles`)、`subprocess.run()`(使用`asyncio.create_subprocess_exec()`)。不得不运行阻塞代码时:使用`asyncio.get_event_loop().run_in_executor(None, blocking_function, args)`在线程池中运行阻塞代码,而不阻塞事件循环。错误混合同步和异步:你不能在常规函数中`await`。如果你有一个需要调用异步代码的同步函数,使用`asyncio.run()`(只在顶层——不能从正在运行的事件循环内调用`asyncio.run()`)。httpx模式(正确的异步HTTP客户端):
“`python
import httpx
import asyncio
async def fetch_many(urls: list[str]) -> list[str]:
async with httpx.AsyncClient() as client:
tasks = [client.get(url) for url in urls]
responses = await asyncio.gather(*tasks)
return [r.text for r in responses]
“`
异步上下文管理器模式(`async with`):许多异步资源(数据库连接、HTTP客户端、文件句柄)需要异步上下文管理器——`async with`块将`__aenter__`和`__aexit__`作为协程调用。始终使用它们;它们确保正确的资源清理。调试asyncio:使用`PYTHONASYNCIODEBUG=1`运行以启用慢回调检测(当回调阻塞事件循环超过100ms时发出警告)。开发时使用`asyncio.run(main(), debug=True)`。



