在 Python 的异步世界里,asyncio 是处理高并发 I/O 任务的利器。它通过单线程 + 事件循环(Event Loop)+ 协作式多任务的模型,高效地管理成百上千个网络请求、文件读写等操作。
但现实很骨感:我们常常需要调用一些同步函数------它们可能是老旧的第三方库、阻塞的数据库驱动,或是复杂的 CPU 计算。如果直接在协程中调用这些函数,哪怕只花 1 秒钟,也会卡死整个事件循环,导致所有其他协程"集体罢工"。
💡 核心问题:
同步 = 阻塞 = 事件循环冻结
那么,有没有办法让这些"拖后腿"的同步代码不拖慢整个系统?答案是肯定的!asyncio 提供了两种官方推荐方案:
- loop.run_in_executor()(适用于所有 Python 3.4+)
- asyncio.to_thread()(Python 3.9+ 引入,更简洁)
下面我们就一步步揭开它们的面纱,并告诉你何时该用哪个。
❌ 错误示范:直接调用同步函数
先看一个典型的反面教材:
python
import asyncio
import time
def blocking_io():
print(">>> 开始同步 I/O...")
time.sleep(3) # 模拟耗时操作
print("<<< 同步 I/O 结束")
return "I/O 结果"
async def main_task():
print("主协程:开始执行...")
result = blocking_io() # ⚠️ 直接调用!会阻塞事件循环!
print(f"收到结果: {result}")
async def other_task():
for i in range(6):
await asyncio.sleep(0.5)
print(f"辅助协程运行中 ({i+1})...")
async def main():
await asyncio.gather(main_task(), other_task())
asyncio.run(main())
运行效果:
你会发现 other_task 在前 3 秒完全"静音"------因为 blocking_io() 卡住了整个事件循环!
🚫 结论:永远不要在协程中直接调用耗时的同步函数!
✅ 正确姿势一:loop.run_in_executor()
这是 asyncio 最经典的解决方案。它的原理很简单:
把同步函数"扔"到另一个线程去跑,主线程继续处理其他协程。
基本用法
python
import asyncio
import time
from concurrent.futures import ThreadPoolExecutor
def blocking_io():
print(">>> 工作线程:开始同步 I/O...")
time.sleep(3)
print("<<< 工作线程:同步 I/O 结束")
return "I/O 结果"
async def main_task():
loop = asyncio.get_running_loop()
# 使用默认线程池执行 blocking_io
result = await loop.run_in_executor(None, blocking_io)
print(f"主协程:收到结果: {result}")
async def other_task():
for i in range(6):
await asyncio.sleep(0.5)
print(f"辅助协程运行中 ({i+1})...")
async def main():
await asyncio.gather(main_task(), other_task())
asyncio.run(main()) # 辅助任务不再被卡住!
✅ 关键点:
- None 表示使用默认的 ThreadPoolExecutor
- run_in_executor 返回一个 asyncio.Future,可直接 await
- 事件循环在等待结果期间不会阻塞,其他协程照常运行
传递参数
想给同步函数传参?很简单,后续参数会自动透传:
python
def task(name, delay):
time.sleep(delay)
return f"{name} 完成"
async def main():
loop = asyncio.get_running_loop()
fut1 = loop.run_in_executor(None, task, "A", 2)
fut2 = loop.run_in_executor(None, task, "B", 1)
results = await asyncio.gather(fut1, fut2)
print(results) # ['A 完成', 'B 完成']
自定义执行器(高级用法)
- I/O 密集型 → 用 ThreadPoolExecutor(默认)
- CPU 密集型 → 用 ProcessPoolExecutor(绕过 GIL)
python
def cpu_task(n):
return sum(i*i for i in range(n))
async def main():
loop = asyncio.get_running_loop()
with concurrent.futures.ProcessPoolExecutor() as pool:
result = await loop.run_in_executor(pool, cpu_task, 10_000_000)
print(result)
✅ 正确姿势二:asyncio.to_thread()(Python 3.9+ 推荐)
如果你使用的是 Python 3.9 或更高版本,有一个更简洁、更现代的替代方案:
python
import asyncio
import time
def blocking_io(delay):
print(f">>> 工作线程:开始阻塞 {delay} 秒...")
time.sleep(delay)
return f"完成于 {delay} 秒后"
async def main():
# 直接 await to_thread,无需获取 loop!
result = await asyncio.to_thread(blocking_io, 3)
print(f"主协程:{result}")
优势
| 特性 | loop.run_in_executor(None, ...) |
asyncio.to_thread(...) |
|---|---|---|
| 代码简洁度 | 需要先获取 loop |
直接调用,一行搞定 |
| 可读性 | 稍显底层 | 更符合"异步直觉" |
| 兼容性 | Python 3.4+ | Python 3.9+ |
| 功能 | 支持任意 Executor |
仅限线程池(I/O 场景) |
✅ 官方建议:
对于绝大多数 I/O 密集型阻塞任务(如文件读写、数据库查询、HTTP 请求等),优先使用 asyncio.to_thread()。
🆚 两者如何选择?
| 场景 | 推荐方案 |
|---|---|
| Python < 3.9 | 只能用 loop.run_in_executor() |
| Python ≥ 3.9,且是 I/O 阻塞 | ✅ 首选 asyncio.to_thread() |
需要自定义线程池(如控制 max_workers) |
run_in_executor + 自定义 ThreadPoolExecutor |
| CPU 密集型任务 | run_in_executor + ProcessPoolExecutor |
🔍 技术细节补充
- asyncio.to_thread(func, *args) 内部本质上就是调用 loop.run_in_executor(None, func, *args),但它还额外做了两件事:
- 自动传播 contextvars.Context(上下文变量可在子线程中访问)
- 返回一个标准的协程对象,语义更清晰
- 默认线程池由 asyncio.run() 自动管理,无需手动关闭。但如果你手动创建了 ThreadPoolExecutor 或 ProcessPoolExecutor,务必用 with 语句确保资源释放。
✅ 总结
- 不要让同步代码阻塞事件循环!
- 对于 I/O 阻塞:用 asyncio.to_thread()(Python 3.9+)或 loop.run_in_executor(None, ...)。
- 对于 CPU 阻塞:用 loop.run_in_executor(ProcessPoolExecutor(), ...)。
- to_thread 是未来趋势,代码更干净;run_in_executor 更灵活,适合高级场景。
掌握这两个工具,你就能在异步世界里"左右逢源",既享受 async/await 的优雅,又不被同步代码拖累!
📌 记住口诀:
I/O 用线程,CPU 用进程;
to_thread 简洁,run_in_executor 灵活。
参考文献
https://docs.python.org/3/library/asyncio-task.html
https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.run_in_executor
https://runebook.dev/zh/docs/python/library/asyncio-task/running-in-threads