问题与目标
大量网络等待如果每个请求占一个线程,资源成本会持续增长。asyncio 通过事件循环在单线程内协作调度任务,适合支持异步接口的高并发 I/O。
完成标准:能运行多个协程任务,为任务设置超时并正确处理取消;明确同步阻塞代码不能因为放进 async def 就自动异步。
核心概念
调用 async def 函数得到协程对象,只有 await、创建任务或交给事件循环后才执行。协程在 await 处主动让出控制权,因此长时间计算或同步 I/O 会阻塞整个事件循环。
asyncio.create_task() 把协程安排为可并发推进的任务。超时通常通过取消任务实现;取消会在协程中触发 CancelledError,资源清理应放在 finally 中,清理后继续传播取消。
批量任务可使用 asyncio.gather() 等待。它按传入顺序返回结果,而不是按完成顺序;其中一个任务失败时,应明确其他任务是继续、取消还是收集异常,不能假设调用方会自动得到理想的清理行为。
可运行实现
import asyncio
async def fetch(name: str, delay: float) -> str:
print(f"start {name}")
try:
await asyncio.sleep(delay)
return f"done {name}"
finally:
print(f"cleanup {name}")
async def main() -> None:
fast = asyncio.create_task(fetch("fast", 0.2))
slow = asyncio.create_task(fetch("slow", 2.0))
print(await fast)
try:
print(await asyncio.wait_for(slow, timeout=0.5))
except TimeoutError:
print("slow timed out")
if __name__ == "__main__":
asyncio.run(main())
输出顺序会体现两个任务先后启动,fast 正常完成,slow 超时后仍执行清理逻辑。
同步阻塞函数需要改用真正的异步客户端,或临时交给线程:
async def call_blocking(argument: str) -> str:
return await asyncio.to_thread(blocking_function, argument)
to_thread 是兼容方案,不会把函数本身改造成非阻塞,也不适合借此加速纯 Python 计算。
下游接口有容量限制时,用信号量控制并发,而不是一次创建无限任务:
import asyncio
async def limited_work(name: str, semaphore: asyncio.Semaphore) -> str:
async with semaphore:
await asyncio.sleep(0.1)
return name
async def run_batch() -> list[str]:
semaphore = asyncio.Semaphore(3)
jobs = [limited_work(str(index), semaphore) for index in range(10)]
return await asyncio.gather(*jobs)
if __name__ == "__main__":
print(asyncio.run(run_batch()))
主动取消也要等待任务收尾:调用 task.cancel() 后再 await task,并在控制边界处理 CancelledError。否则清理逻辑可能还没完成,程序就退出了。
常见问题与排查
- 打印协程对象而没有结果:忘记
await。 - 在异步函数里调用同步 HTTP 客户端:事件循环仍被阻塞,应使用异步客户端。
- 创建任务后丢失引用:调用方难以等待、取消和读取异常;保存任务并明确收尾。
- 吞掉
CancelledError:上层会误以为取消失败。通常在finally清理后让异常继续传播。 - 在已有事件循环中调用
asyncio.run():框架内部应直接await,程序最外层才使用asyncio.run()。
小结
异步的关键是协作式让出控制权,而不是函数名前的 async。一个可靠异步边界必须同时考虑并发数量、超时、取消、异常和资源清理。
License: CC BY-NC 4.0
Updated 3 hours ago
Was this article helpful? Give it a like.
0 comments


