[Python] asyncio
asyncio 入门到实践
在 Python 并发编程里,asyncio 是一个绕不开的话题。很多人第一次接触它时,往往会被一堆概念劝退:协程、事件循环、任务、Future、取消、超时、线程切换……
但如果换个角度看,asyncio 本质上只是在解决一个问题:
当程序里有大量“等待”的操作时,如何让一个线程尽可能高效地把这些任务都调度起来。
这篇文章不追求面面俱到,而是希望用一篇内容把 asyncio 最常见的使用方式、核心概念和实际开发中的注意事项讲清楚。读完之后,你至少会明白:
- async / await 到底在做什么
- 事件循环是如何调度任务的
- 什么场景应该用 asyncio
- 哪些写法最容易踩坑
什么是 asyncio?
asyncio 是 Python 标准库中用于编写单线程并发程序的模块,它基于事件循环(Event Loop)机制,配合 async/await 语法,可以高效处理大量 I/O 密集型任务。
它尤其适合以下场景:
- 网络请求
- Web 服务
- 消息队列消费
- 长连接通信
- 异步爬虫
- 定时任务调度
- 大量文件或数据库 I/O 操作
需要注意的是:
asyncio 擅长的是 I/O 密集型并发,不是 CPU 密集型并行。
如果你的任务主要是大量计算,asyncio 往往不是最佳选择。
先建立一个整体认知
在学习具体 API 之前,先看一张图,理解 asyncio 的运行机制。

你可以把它理解成这样:
- async def 定义的是“可以暂停和恢复的函数”
- await 的意思是“我现在要等一下,你先去执行别的任务”
- 事件循环就是那个“总调度员”,负责在各个任务之间切换
整个过程通常只发生在一个线程里
所以,asyncio 的高效,不是因为它让代码“同时执行”,而是因为它让程序在等待 I/O 时“不闲着”。
基础用法
- 定义协程
使用 async def 定义协程函数。
import asyncio
async def say_hello():
await asyncio.sleep(1)
print("Hello")
这里的 asyncio.sleep(1) 并不会阻塞线程,而是告诉事件循环:“我 1 秒后再继续,先去做别的事。”
- 运行协程
通常使用 asyncio.run() 作为程序入口。
asyncio.run(say_hello())
它会:
- 创建事件循环
- 执行协程
- 等待协程完成
- 最后关闭事件循环
对于绝大多数脚本程序来说,这是最推荐的启动方式。
- 创建任务,实现并发
如果想让多个协程并发运行,可以使用 asyncio.create_task()。
async def main():
task = asyncio.create_task(say_hello())
await task
asyncio.run(main())
create_task() 会把协程包装成一个 Task,并立即交给事件循环调度。
如果没有它,协程只是“定义好了”,并不会自动并发执行。
- 等待多个协程
使用 asyncio.gather()
async def main():
results = await asyncio.gather(
coro1(),
coro2(),
coro3()
)
print(results)
适合场景:
- 希望并发执行多个任务
- 希望等它们全部完成
- 希望按传入顺序拿到结果
使用 asyncio.wait()
done, pending = await asyncio.wait(
[task1, task2],
return_when=asyncio.FIRST_COMPLETED
)
适合场景:
- 只关心谁先完成
- 想更灵活地控制任务状态
- 需要手动处理 done 和 pending
简单理解:
gather() 更偏“批量拿结果”
wait() 更偏“任务状态控制”
- 超时控制
异步任务最怕无限等待,所以超时控制非常重要。
try:
result = await asyncio.wait_for(long_task(), timeout=5.0)
except asyncio.TimeoutError:
print("任务超时")
wait_for() 会在超时后抛出 TimeoutError,并尝试取消任务。
这在网络请求、外部依赖调用、队列消费等场景里非常常见。
- 异步队列:生产者-消费者模式
asyncio.Queue 是异步程序中非常常用的数据结构。
queue = asyncio.Queue(maxsize=10)
await queue.put(item)
item = await queue.get()
queue.task_done()
它的好处是:
- 队列满时,put() 自动挂起
- 队列空时,get() 自动挂起
- 非常适合任务分发和流式消费
常见应用包括:
- 爬虫任务调度
- 消息消费系统
- 后台任务流水线
- 多协程解耦
- 在线程中运行同步代码
协程里最忌讳的事情之一,就是直接调用阻塞型同步代码。
例如下面这种写法就有问题:
async def bad():
requests.get("https://example.com")
因为 requests.get() 会阻塞线程,导致整个事件循环卡住。
正确做法是把它扔到线程池中:
result = await asyncio.to_thread(requests.get, "https://example.com")
或者使用更底层的方式:
loop = asyncio.get_running_loop()
result = await loop.run_in_executor(None, blocking_func, arg1, arg2)
推荐理解为:
- to_thread() 更现代,也更简洁
- run_in_executor() 更底层,控制更细
- 跨线程提交协程
如果事件循环正在某个线程里运行,而另外一个线程需要向它提交协程,就要使用:
future = asyncio.run_coroutine_threadsafe(queue.put(event), loop)
future.result(timeout=1)
它的作用是:
- 从其他线程安全地把协程提交到指定事件循环中执行。
这在下面这些场景里很常见:
- 第三方同步回调运行在其他线程中
- 线程池任务执行完后要通知异步系统
- 后台线程要把结果送回异步队列
如果不用它,而是直接跨线程操作异步对象,通常会报错或者出现不稳定行为。
- 同步原语
asyncio 也提供了一些常见的并发控制工具:
asyncio.Lock
asyncio.Event
asyncio.Semaphore
asyncio.Condition
例如:
lock = asyncio.Lock()
async with lock:
# 临界区
...
注意一点:
这些是给协程之间用的,不是给线程之间用的。
- 子进程管理
asyncio 也支持异步地启动和管理子进程。
proc = await asyncio.create_subprocess_exec(
"ls",
stdout=asyncio.subprocess.PIPE
)
stdout, _ = await proc.communicate()
适合场景:
- 异步执行命令行工具
- 读取外部程序输出
- 构建流式处理管道
- 网络流(Streams)
如果你需要做 TCP 通信,asyncio 也提供了原生支持。
reader, writer = await asyncio.open_connection("127.0.0.1", 8888)
服务端可以使用 asyncio.start_server()。
适用场景包括:
- TCP 客户端/服务端
- 长连接通信
- 自定义协议实现
- 简单代理服务
- 获取当前事件循环与调试
在协程中可以这样获取当前运行中的事件循环:
loop = asyncio.get_running_loop()
如果当前线程没有运行中的事件循环,会直接抛异常。
调试时,还可以开启 asyncio 调试模式:
loop.set_debug(True)
或者设置环境变量:
PYTHONASYNCIODEBUG=1
这对排查以下问题很有帮助:
- 协程忘记 await
- 任务没有正确回收
- 回调执行过慢
- 某些 Future 状态异常
- 屏蔽取消操作:asyncio.shield()
有时候,某些逻辑即使外层任务被取消,也希望它尽量执行完成,比如:
- 写审计日志
- 保存状态
- 提交事务
- 清理资源
这时可以用 asyncio.shield():
task = asyncio.create_task(sensitive_coro())
result = await asyncio.shield(task)
它的作用不是“让任务永远无法取消”,而是:
避免外层取消直接传播到这个 await 的对象。
所以它适合“关键收尾逻辑”,但不能滥用,否则会让取消机制失去意义。
什么情况下应该使用 asyncio?
很多人学了 asyncio 之后,最常见的问题不是“怎么写”,而是“该不该用”。
我的建议很简单:
- 适合使用 asyncio 的场景
- 有大量网络请求
- 有大量 I/O 等待
- 有很多任务需要高并发调度
- 需要维护大量连接
- 任务之间切换频繁,但单个任务大部分时间都在等待
比如:
- 爬虫
- Web 服务
- 聊天系统
- 网关服务
- 消息消费系统
- 异步任务处理器
不适合使用 asyncio 的场景,主要是 CPU 密集型计算,代码依赖大量阻塞式第三方库,任务量不大,异步收益很低
团队对异步模型不熟,维护成本远高于收益
比如:图像处理,大规模数值计算,复杂机器学习推理,本质上是串行脚本的小工具
一句话总结:
如果瓶颈在“等待”,就考虑 asyncio;如果瓶颈在“计算”,优先考虑多进程或其他并行方案。
常见坑
这一部分很重要。很多人不是不会写 asyncio,而是会写但写得“像同步代码”,结果表面上用了异步,实际上还是低效甚至有 bug。
- 只写了 async def,却没有真正并发
很多初学者以为写成 async def 就自动并发了,其实不是。
async def main():
await task1()
await task2()
这段代码仍然是顺序执行的。只有在适当的时候使用 create_task() 或 gather(),才会真正并发调度。
async def main():
await asyncio.gather(task1(), task2())
- 在协程里调用阻塞函数
这是最常见、也最隐蔽的坑。
async def main():
time.sleep(3) # 错误示例
time.sleep() 会直接阻塞线程,导致整个事件循环停住,所有协程都无法继续执行。
正确写法应该是:
await asyncio.sleep(3)
如果必须调用阻塞式同步函数,就用 asyncio.to_thread()。
- 忘记 await
下面这种情况非常常见:
async def main():
say_hello()
这不会真正执行协程,而只是创建了一个协程对象,通常还会伴随告警:coroutine was never awaited。
正确写法是:
await say_hello()
或者:
asyncio.create_task(say_hello())
- 创建了任务,却没有回收
例如:
asyncio.create_task(worker())
这样虽然任务跑起来了,但如果你后面完全不管它,就可能出现:
- 异常被吞掉
- 程序结束时任务未完成
- 调试时很难定位问题
更稳妥的做法是:
- 保存任务引用
- 在合适的时机 await
- 或统一管理任务生命周期
task = asyncio.create_task(worker())
await task
- 把 asyncio 当成多线程使用
asyncio 是单线程并发模型,不是多线程模型。
所以:
- asyncio.Lock 不是线程锁
- 协程安全不等于线程安全
- 跨线程不能随便操作事件循环对象
如果涉及线程和事件循环交互,要用 run_coroutine_threadsafe() 或者线程安全队列等机制。
- 取消任务时没有做好清理
异步任务是可以被取消的,所以代码里最好考虑取消时的清理逻辑。
async def worker():
try:
await asyncio.sleep(10)
except asyncio.CancelledError:
print("任务被取消,开始清理")
raise
如果忽略这一点,可能导致:资源未释放,状态不一致以及完成操作残留
- 对 gather() 的异常传播理解不清
await asyncio.gather(task1(), task2(), task3())
默认情况下,只要其中一个任务抛异常,gather() 就会把异常抛出来。
如果你希望“即使某个任务失败,也收集其他任务结果”,可以使用:
results = await asyncio.gather(
task1(), task2(), task3(),
return_exceptions=True
)
但要注意,这时返回结果里可能混有异常对象,需要你自己判断处理。
- 滥用 shield(),让取消失控
shield() 适合保护关键收尾逻辑,但如果什么都用它包起来,任务就很难被正常取消,程序退出时也可能拖很久。
原则上:
- 关键清理可以保护
- 主体业务逻辑不要轻易屏蔽取消
- 在不该用异步的地方强行用异步
有些项目明明只有两三个请求,或者逻辑几乎全是计算,却为了“看起来高级”硬上 asyncio,结果代码复杂度显著上升,收益却很有限。
技术选型最怕“为了用而用”。异步不是目标,解决问题才是目标。
六、关于 run_coroutine_threadsafe(),到底什么时候该用?
这个 API 很多人看文档时能看懂,但实际写代码时还是容易混乱。
它的适用条件非常明确:
当前线程不是事件循环线程,但你需要把一个协程提交到那个事件循环中执行。
例如:
- 某个同步 SDK 的回调发生在后台线程
- 线程池任务执行完成,要把结果塞回异步队列
- 非异步线程想触发某个异步发送逻辑
示例:
future = asyncio.run_coroutine_threadsafe(queue.put(event), loop)
future.result(timeout=1)
它解决的问题不是“少写一个 await”,而是:跨线程安全地调度协程。
如果你的代码本身已经在协程里,那通常直接 await queue.put(event) 就够了,没必要使用它。
所以可以这样记:
在协程里:直接 await
在线程里:run_coroutine_threadsafe()
实战选型建议
如果把常见开发场景做一个简单归纳,可以参考下面这套思路:
简单脚本
使用:
asyncio.run()
asyncio.gather()
适合快速并发执行多个 I/O 任务。
Web 服务
结合:FastAPI,aiohttp,让请求处理函数天然支持协程模型。
长时间运行的后台任务
使用:
asyncio.create_task()
asyncio.Queue
超时控制
取消机制
适合任务调度、消息消费、异步工作流。
混合同步代码
使用:
asyncio.to_thread()
run_in_executor()
把阻塞逻辑隔离出去,避免卡住事件循环。
跨线程协作
使用:
asyncio.run_coroutine_threadsafe()
不要直接跨线程操作异步对象。
总结
如果你想真正学会 asyncio,不需要一开始就钻进所有底层细节。先抓住这三件事就够了:
- async def 定义协程
- await 表示挂起等待
- 事件循环负责调度任务
在此基础上,再逐步掌握:
- create_task() 做并发
- gather() 管理批量任务
- wait_for() 做超时控制
- Queue 组织任务流
- to_thread() 兼容同步阻塞代码
- run_coroutine_threadsafe() 处理跨线程协作
说到底,asyncio 不是什么“高深黑魔法”,它只是提供了一种更高效的 I/O 并发编程方式。真正重要的,不是记住多少 API,而是理解一句话:当程序在等待时,如何不要浪费这段时间。
参考:
Python 异步编程入门
事件循环

浙公网安备 33010602011771号