同步代码会直接卡住整个asyncio事件循环,因其阻塞调用不主动让出控制权,导致其他协程无法调度;必须通过run_in_executor或专用异步库移出主线程。

同步代码会直接卡住整个 asyncio 事件循环,没有“避免”的余地——只有把阻塞操作移出事件循环主线程。
为什么 time.sleep() 或 requests.get() 会让 async 代码变慢?
asyncio 的事件循环运行在单线程中,它靠协作式调度(cooperative scheduling)切换协程。一旦遇到 time.sleep()、requests.get()、json.loads()(超大字符串)、subprocess.run() 等同步阻塞调用,CPU 控制权就交不出去,其他协程全部挂起,哪怕它们本可以并发处理 I/O。
常见现象:asyncio.sleep(0) 不生效、多个 await asyncio.create_task(...) 实际串行执行、HTTP 请求响应时间翻倍甚至超时。
- Python 的 GIL 不是主因,关键是“不 yield 控制权”
- asyncio 本身不拦截系统调用,
os.read()这类底层阻塞也会卡死循环 - 第三方库若没提供 async 接口(如旧版
psycopg2),直接 await 也没用
用 asyncio.to_thread() 拆离 CPU/IO 密集型同步调用
Python 3.9+ 提供了轻量方案:把同步函数扔进线程池执行,主线程继续跑事件循环。这是目前最常用、开销可控的解法。
适用场景:time.sleep() 替代、requests.get()、cv2.imread()、pandas.read_csv()、lxml.etree.fromstring() 等。
import asyncio
import requests
<p>async def fetch_sync():</p><h1>❌ 错误:直接调用会阻塞</h1><pre class="brush:php;toolbar:false;"># return requests.get("https://httpbin.org/delay/2").json()
# ✅ 正确:丢进线程池
loop = asyncio.get_running_loop()
result = await loop.run_in_executor(None, requests.get, "https://httpbin.org/delay/2")
return result.json()-
None表示使用默认线程池(concurrent.futures.ThreadPoolExecutor),一般够用 - 若需控制线程数或复用,可提前创建 executor 并传入
- 注意:不能用于需要主线程上下文的对象(如某些 GUI 库、TLS 上下文绑定的连接)
用 asyncio.to_process() 处理真正危险的 CPU 绑定任务
当同步代码是纯计算(如加密哈希、科学计算、正则爆炸回溯),线程池仍受 GIL 限制,无法并行提速,还可能拖慢 I/O 协程。此时应换进程。
Python 3.12+ 引入 asyncio.to_process();3.12 之前需手动封装 ProcessPoolExecutor:
from concurrent.futures import ProcessPoolExecutor import hashlib <p>def cpu_bound_work(data): return hashlib.sha256(data.encode()).hexdigest()</p><p>async def hash_async(data): loop = asyncio.get_running_loop() with ProcessPoolExecutor() as pool: return await loop.run_in_executor(pool, cpu_bound_work, data) </p>
- 进程开销比线程大,别对小数据或高频调用滥用
- 参数和返回值必须可 pickle,numpy 数组需额外处理
- Windows 下注意
if __name__ == "__main__"保护,否则 spawn 失败
哪些情况根本不能“绕过”,只能换库?
有些阻塞不是调用方式问题,而是底层设计决定的。强行套 to_thread 只是掩盖问题,还可能引发资源泄漏或状态错乱。
典型例子:sqlite3.connect() 默认开启 WAL 模式时的写锁、open(file, "w") 同步写盘、serial.Serial() 阻塞读取。
- 优先查文档:是否有官方 async 版本(如
aiosqlite、aiofiles、pyserial-asyncio) - 数据库类操作强烈建议换 async 驱动,否则连接池、事务、超时都难控制
- 自定义协议或硬件通信,若底层只提供阻塞 API,
to_thread是唯一选择,但务必加超时和重试
真正容易被忽略的是:即使用了 to_thread,如果线程池太小或同步函数内部又调了另一个阻塞调用(比如某 SDK 内部用 urllib.request.urlopen()),照样卡死。得一层层 inspect 调用栈。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











