asyncio事件循环调度开销远小于threading,因协程切换在用户态、仅指针跳转与状态标记(纳秒级),而线程切换需内核参与、保存完整上下文并争夺GIL(几十微秒);1000并发HTTP请求下,asyncio内存占用不足threading的5%,且无OOM或调度抖动。

asyncio事件循环比threading调度开销小得多
Python多线程的调度由操作系统完成,每次线程切换都要保存/恢复寄存器、栈、CPU上下文,还要处理GIL加锁/释放——哪怕只是I/O等待后唤醒,这个过程在CPython里平均耗时几十微秒。而asyncio的事件循环完全在用户态运行,协程挂起和恢复只涉及几个指针跳转和状态标记,开销通常在纳秒级。
实际测试中,1000个并发HTTP请求:用threading启动1000个线程会直接触发系统OOM或被OS限制;用asyncio+aiohttp跑1000个协程,内存占用不到线程方案的5%,且无上下文切换抖动。
- 线程切换:依赖OS内核,受GIL制约,有真实硬件开销
- 协程切换:纯Python对象状态变更,
await只是把当前协程对象塞进就绪队列,再从队列取下一个 - 注意:
asyncio.run()内部只启动一个OS线程,所有协程都在这一个线程里流转
异步IO不触发GIL释放/重入的反复争夺
多线程做网络请求时,虽然requests.get()底层会释放GIL等待socket响应,但一旦响应到达,线程要重新抢GIL才能解析响应体、执行回调、更新变量——这时候如果其他线程也在争GIL,就会排队等待。而aiohttp这类异步库全程不碰GIL:socket操作通过select/epoll注册事件,响应就绪后由事件循环直接调用回调函数,整个过程GIL始终由主线程持有,没有释放-重入的锁竞争。
- 同步阻塞IO(如
requests):释放GIL → 等待 → 重获GIL → 处理数据 - 异步非阻塞IO(如
aiohttp):GIL一直持有 → 注册fd → 事件就绪 → 直接处理 - 关键点:
await本身不释放GIL,它只是让出协程控制权,不是让出线程控制权
协程内存占用远低于线程,尤其高并发场景
每个Python线程默认分配约8MB栈空间(Windows下可能更大),即使空闲也占着。而协程对象本身只有几百字节,栈空间是动态增长的,实际使用中一个协程平均只占几KB。这意味着同样1GB内存,能同时活跃的协程数可能是线程数的上百倍。
华为云 Python SDK 查询工具。自动发现已安装 SDK 的服务包、Client类、API 方法、入参详情、类信息(__init__ 参数、属性、类型、描述),支持关键字搜索,快速定位目标 API 使用方法。
立即学习“Python免费学习笔记(深入)”;
- 创建1000个
threading.Thread:至少消耗8GB虚拟内存(大部分未提交,但OS要预留) - 创建1000个
async def协程:内存占用≈10MB左右,且可全部同时“运行” - 陷阱:
asyncio.create_task()不会立刻执行,但协程对象已生成;大量未await的任务会堆积在事件循环中,导致延迟升高
为什么你测不出差距?常见干扰项
很多人拿简单脚本对比threading和asyncio,发现耗时差不多,甚至线程更快——大概率是因为没压到并发瓶颈,或者混用了阻塞调用。
- 用
time.sleep()代替真实IO:它不释放GIL,线程版会假性并行,异步版反而因事件循环调度多了一层间接 - 混用
requests在async def里:协程立即被阻塞,退化成同步串行 - 没用
asyncio.gather()并发启动任务:写成await f1(); await f2()就是纯同步 - 单次请求测速:网络延迟波动远大于调度开销,看不出差异;必须测100+并发才暴露本质
真正拉开差距的从来不是单个请求,而是当连接数涨到几百上千时,线程方案开始swap、OOM、调度延迟飙升,而异步方案依然稳定吞吐——这个拐点,往往就在你忽略的那几百个协程上。

















