注意点:协程里面跑必须跑同步代码的时候,必须放进线程池或者进程池,然后将线程池或者进程池进行封装,使协程可以 异步等待 后台线程或后台进程执行任务 -----应该是完整的,后续跟协程一块总结
彻底搞懂:asyncio 结合线程池处理阻塞 IO(完整版可直接发博客)
在 Python 异步开发中,asyncio 依托单线程事件循环实现高并发,但它天生无法兼容传统同步阻塞代码。当项目中不得不使用 requests、同步文件读写、老旧第三方库时,ThreadPoolExecutor 就成了主流解决方案。本文完整讲解使用场景、底层原理、代码实战、核心误区、强制规范,帮你彻底理清协程与线程池的协作关系。
一、核心前置结论
asyncio运行在单一线程中,一旦执行同步阻塞代码,整个事件循环会被冻结,所有协程停止调度。ThreadPoolExecutor提供独立线程池,专门承载同步阻塞任务,实现主线程(事件循环)与阻塞任务解耦。loop.run_in_executor()是二者的桥梁,将线程任务封装为asyncio.Future,让协程可以通过await异步等待执行结果。- 重要误区纠正:该方案并不是“把线程池当成协程使用”,而是协程异步等待后台线程执行任务,线程和协程本质是两套不同的并发模型。
二、asyncio 关键注意点(强制规范)
核心注意点(最标准、最准确、最简洁)
在 asyncio 协程内部,如果必须运行同步阻塞代码(如同步网络请求、同步文件IO、第三方同步库),
绝对不能直接执行,否则会阻塞整个事件循环,导致所有协程卡死。
正确做法只有一种:
把同步代码丢进线程池或进程池执行,
然后将线程池/进程池封装成可 await 的异步对象,
让协程可以异步等待后台线程/进程完成任务,不阻塞主线程。
一句话金句
协程不跑阻塞代码,阻塞代码不进协程;
必须跑就丢线程/进程池,封装后让协程异步等待。
详细规范说明
- 协程所在的主线程 = 事件循环唯一线程
这个线程一旦被同步代码阻塞,整个异步程序都会停止。 - 同步阻塞代码必须剥离
如:requests、time.sleep()、open()、同步数据库驱动等。 - 解决方案:线程池 / 进程池隔离
- IO 密集型:用
ThreadPoolExecutor - CPU 密集型:用
ProcessPoolExecutor
- IO 密集型:用
- 封装成异步可等待对象
使用loop.run_in_executor()将线程/进程池包装成Future,
让协程可以await等待,实现异步非阻塞。
三、为什么必须搭配线程池?
3.1 纯 asyncio 无法处理同步阻塞代码
asyncio 的高并发能力建立在非阻塞 IO + 协程主动切换之上。如果在协程内直接调用同步阻塞函数,当前唯一的主线程会被卡住,事件循环无法调度其他协程,并发特性完全失效。
错误示例(直接在协程中使用同步请求):
import asyncio
import requests
async def bad_request():
# 同步阻塞调用,卡死整个事件循环
resp = requests.get("https://httpbin.org/get")
print(f"状态码:{resp.status_code}")
async def main():
# 看似并发,实际串行执行
await asyncio.gather(bad_request(), bad_request())
asyncio.run(main())
运行现象:两个请求串行执行,无任何并发效果,程序整体响应缓慢。
3.2 线程池的核心作用
将同步阻塞任务剥离到独立子线程执行,主线程的事件循环保持正常运转:
- 阻塞逻辑在线程池内运行,不占用主线程;
- 事件循环继续调度其他就绪协程;
- 任务执行完毕后,通过回调将结果回传给协程。
四、底层原理与伪代码解析
4.1 run_in_executor 工作流程
loop.run_in_executor 是衔接协程与线程池的核心方法,内部分为三步:
- 将同步函数提交给
ThreadPoolExecutor执行; - 把线程池的任务对象包装为
asyncio.Future; - 注册回调,线程任务完成后,通过
set_result/set_exception唤醒等待的协程。
4.2 核心逻辑伪代码
# 模拟 run_in_executor 底层实现
def run_in_executor(executor, func, *args):
# 1. 同步函数提交到线程池
thread_future = executor.submit(func, *args)
# 2. 创建 asyncio 可等待对象
async_future = loop.create_future()
# 3. 线程任务完成后的回调
def done_callback(fut):
if fut.cancelled():
async_future.cancel()
elif fut.exception():
async_future.set_exception(fut.exception())
else:
async_future.set_result(fut.result())
thread_future.add_done_callback(done_callback)
return async_future
五、实战代码:协程 + 线程池标准用法
5.1 完整可运行示例
import asyncio
import requests
from concurrent.futures import ThreadPoolExecutor
# 全局创建线程池,复用线程,避免频繁创建销毁开销
executor = ThreadPoolExecutor(max_workers=10)
async def safe_request(url):
# 获取当前运行的事件循环
loop = asyncio.get_running_loop()
# 将同步阻塞函数交由线程池执行,协程异步等待结果
resp = await loop.run_in_executor(executor, requests.get, url)
print(f"请求 {url} 状态码:{resp.status_code}")
return resp
async def main():
url_list = [
"https://httpbin.org/get",
"https://httpbin.org/delay/1",
"https://httpbin.org/get"
]
# 并发执行多个协程
tasks = [safe_request(url) for url in url_list]
await asyncio.gather(*tasks)
if __name__ == "__main__":
asyncio.run(main())
5.2 三种方案效果对比
| 实现方案 | 执行方式 | 并发能力 | 事件循环状态 |
|---|---|---|---|
| 协程直接调用同步函数 | 串行执行 | 无并发 | 完全阻塞 |
| 协程 + ThreadPoolExecutor | 多线程并行 | 高并发 | 正常运行 |
| 纯异步库(aiohttp) | 协程调度并发 | 最高并发 | 正常运行 |
六、关键注意事项(避坑指南)
6.1 线程池配置建议
max_workers无需设置过大,参考 CPU 核心数的 2~4 倍 即可,线程过多会增加系统调度开销;- 全局复用同一个线程池,不要在协程内反复创建/销毁线程池。
6.2 适用任务类型
- ✅ 推荐:IO 密集型任务(网络请求、本地文件读写、同步数据库查询);
- ❌ 禁止:CPU 密集型计算任务,受 Python GIL 全局解释器锁限制,多线程无法实现并行,反而降低性能。
6.3 异常捕获
线程内抛出的异常会被封装到 asyncio.Future 中,必须通过 try/except 捕获:
async def safe_request(url):
loop = asyncio.get_running_loop()
try:
resp = await loop.run_in_executor(executor, requests.get, url)
return resp
except Exception as e:
print(f"请求异常:{str(e)}")
return None
6.4 跨线程对象操作限制
线程池中的同步代码,禁止直接操作 asyncio 的 Task、Future、事件循环对象,会引发线程安全问题。
七、核心误区深度解读
很多开发者会误以为「线程池被当成协程来使用」,这是典型理解偏差,下面清晰区分二者本质:
7.1 协程与线程的本质差异
- 协程:运行在单一线程内,属于协作式并发。依靠
await主动让出执行权,同一时刻只有一个协程在运行,切换开销极小。 - 线程:由操作系统内核调度,属于抢占式并发。多个线程可以在多核 CPU 上真正并行执行,线程切换存在内核态开销。
7.2 run_in_executor 的真实逻辑
该方法不会把线程转换成协程,完整逻辑如下:
- 协程将同步阻塞任务外包给后台线程池执行;
- 当前协程触发
await后主动挂起,事件循环调度其他协程; - 后台线程独立运行阻塞代码,执行完成后通过回调唤醒原协程。
通俗总结
协程是主线程里轮流干活的“任务”,线程池是后台独立的“工人”。这套组合只是让协程等待后台工人完工,并不是让工人变成任务。
八、适用场景与最优方案选择
8.1 优先使用 ThreadPoolExecutor 的场景
- 依赖不支持异步的第三方库(如
requests、传统同步 SDK); - 老旧项目改造,无法全量替换为异步 IO;
- 临时兼容少量同步阻塞逻辑,快速实现异步改造。
8.2 最优方案:纯异步开发
如果项目允许,优先使用原生异步库,性能和稳定性最佳:
aiohttp替代requests实现异步网络请求;aiosqlite/asyncpg替代同步数据库驱动;- 全链路使用异步语法,彻底规避线程切换开销。
九、全文总结
- asyncio 强制规则:协程里不能直接跑同步阻塞代码!必须跑 → 丢线程池/进程池 → 封装成 awaitable → 协程异步等待!
asyncio单线程事件循环惧怕同步阻塞,直接调用会导致整体卡死,线程池是重要兼容方案。run_in_executor作为桥接工具,将线程任务封装为异步可等待对象,实现协程与线程的安全协作。- 二者分工明确:线程池承载阻塞 IO,事件循环调度协程,不存在“线程池充当协程”的说法。
- 选型原则:新项目优先使用纯异步库;老项目/第三方库兼容场景,使用
asyncio + ThreadPoolExecutor过渡。 - 线程池仅适配 IO 密集型任务,CPU 密集型任务建议使用进程池。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)