当程序的大部分时间都在等待网络、磁盘或其他外部服务时,单纯增加 CPU 或线程并不能解决所有问题。Python 的异步编程通过生成器、async、await 和 asyncio,让程序在等待 I/O 时处理其他任务,从而提高并发处理能力。
从生成器理解“暂停与恢复”
生成器使用 yield 暂停函数执行,并在下次迭代时恢复。它本身不等同于异步编程,但提供了一个重要思路:函数可以暂时交出控制权,稍后再继续执行。
def numbers():
yield 1
yield 2
yield 3
for number in numbers():
print(number)
在异步程序中,任务也需要暂停和恢复。区别在于,事件循环会在任务等待 I/O 时调度其他任务运行。
async 和 await 做了什么
使用 async def 定义异步函数。调用它时不会立即执行函数体,而是返回一个 coroutine 对象。只有通过 await 或事件循环运行它,这个协程才会真正执行。
await 表示当前任务将在这里等待一个异步结果。在等待期间,事件循环可以执行其他已经准备好的任务。
需要注意,await 只能等待可等待对象,例如协程、任务或某些异步库返回的对象。把同步阻塞函数直接放进 async def,并不会自动变成异步操作。
用 asyncio 并发执行 I/O 任务
下面的示例使用 asyncio.sleep() 模拟网络请求或其他慢速 I/O。asyncio.gather() 会并发调度多个协程,并等待它们全部完成。
将下面内容保存为 async_demo.py,使用 Python 3.9 或更高版本运行:
import asyncio
import time
async def fetch(name: str, delay: float) -> str:
print(f"开始请求 {name}")
await asyncio.sleep(delay) # 模拟网络 I/O
print(f"完成请求 {name}")
return f"{name}: ok"
async def main() -> None:
start = time.perf_counter()
results = await asyncio.gather(
fetch("users", 1.0),
fetch("orders", 1.5),
fetch("inventory", 0.8),
)
elapsed = time.perf_counter() - start
print("结果:", results)
print(f"总耗时: {elapsed:.2f} 秒")
if __name__ == "__main__":
asyncio.run(main())
这三个任务的总耗时接近最长的单个任务,而不是三个延迟之和。真实项目中,可以把 asyncio.sleep() 换成支持异步的 HTTP 客户端、数据库驱动或文件操作库。
如果调用的是普通阻塞函数,例如同步 HTTP 客户端,可以用线程池暂时隔离阻塞调用:
import asyncio
import time
def blocking_work() -> str:
time.sleep(2)
return "完成同步阻塞操作"
async def main() -> None:
result = await asyncio.to_thread(blocking_work)
print(result)
if __name__ == "__main__":
asyncio.run(main())
这不是把同步库变成真正的异步库,而是避免它直接阻塞事件循环。长期运行的服务仍应优先选择原生支持异步的库。
并发不等于无限制地发请求
并发任务过多会耗尽连接池、触发服务端限流,甚至让本地内存和文件描述符达到上限。可以用 asyncio.Semaphore 限制同时运行的任务数量:
import asyncio
async def fetch(item: int, limit: asyncio.Semaphore) -> str:
async with limit:
print(f"处理任务 {item}")
await asyncio.sleep(0.5)
return f"任务 {item} 完成"
async def main() -> None:
limit = asyncio.Semaphore(3)
results = await asyncio.gather(
*(fetch(item, limit) for item in range(10))
)
print("\n".join(results))
if __name__ == "__main__":
asyncio.run(main())
这里最多同时执行三个任务。生产环境还需要根据接口限制设置超时、重试、取消处理和错误记录。
采用 asyncio 前的检查清单
- 确认瓶颈主要来自网络、数据库或其他 I/O,而不是 CPU 密集计算。
- 检查依赖库是否提供异步 API;同步调用会阻塞事件循环。
- 使用
asyncio.gather()或任务组组织并发任务,同时处理异常和取消。 - 为并发数量设置上限,避免连接池和下游服务承压。
- 为每次 I/O 设置超时,并记录任务失败、重试和耗时。
- CPU 密集型工作通常应考虑进程池、独立任务队列或其他并行方案。
异步编程的核心不是到处添加 async,而是让任务在等待期间把执行权交回事件循环。理解生成器的暂停机制,再掌握 async、await 和 asyncio 的调度方式,就能更准确地判断哪些代码适合并发,哪些代码只会增加复杂度。