asyncio在高并发i/o场景下性能远超多线程,因其避开os线程调度与内存硬瓶颈:1000个线程耗内存超200mb且易触发oom,而1000个协程仅约10mb;协程切换在用户态、开销纳秒级,线程切换需内核态、成本高数个数量级。

异步请求比多线程快,不是因为“协程本身更快”,而是因为在高并发 I/O 场景下,asyncio 避开了操作系统线程调度和内存开销的硬瓶颈——线程数一上几百,OSError: can't start new thread 或被 OOM killer 杀掉是常态;而协程轻松跑几千个,常驻内存仅 20–30MB。
为什么 async/await 在 I/O 并发时压倒 threading?
核心差异不在语法,而在执行模型和资源占用:
-
threading.Thread每个线程默认占 8MB 栈空间(Linux),1000 个线程 ≈ 8GB 虚拟内存,实际 RSS 常超 200MB;asyncio协程栈在堆上动态分配,单个协程仅几 KB,1000 个协程内存通常 - 线程切换必须进内核,保存寄存器、页表、栈指针等,耗时从数百纳秒到微秒级;协程切换纯用户态,只保存局部变量和指令位置,本质是
generator状态机跳转,开销可忽略 - 线程在
socket.recv()或time.sleep()时陷入内核等待,期间操作系统仍要维护其上下文;协程await后直接让出控制权给事件循环,不占调度槽位 - GIL 对两者影响不同:线程在 I/O 时释放 GIL,能并发;但协程根本不在意 GIL——它压根没多线程,所有代码都在一个线程里顺序执行,无锁竞争,也无自动共享
requests + threading 和 aiohttp + asyncio 的实操差距
同样发 100 个耗时 2 秒的 HTTP 请求(如本地 Flask 慢接口),两套写法效果截然不同:
调用 Cutout.Pro 视觉处理 API 进行背景移除、人像抠图和照片增强,支持文件上传与图片 URL 输入。
- 用
requests.get()+ThreadPoolExecutor(max_workers=32):总耗时约 6–8 秒(受限于线程池大小和系统线程创建成本),ps -o pid,vsz,comm -p $(pidof python)显示 VSZ 超 500MB - 用
aiohttp.ClientSession.get()+asyncio.gather(*tasks):总耗时稳定在 ~2.1 秒(受最慢请求拖累),VSZ 通常 - 关键陷阱:
requests在协程里直接调用会阻塞整个事件循环——它不是异步库。必须用aiohttp、httpx.AsyncClient或显式用loop.run_in_executor()包裹requests(但失去大部分优势)
生产环境选型不能只看“快”,要看任务混合度
真实服务 rarely 是纯 I/O 或纯 CPU,得拆开看:
-
纯 I/O 密集(如 API 网关、爬虫、实时通知推送):无条件选
asyncio+ 异步生态(aiohttp、httpx、aioredis)。注意:DB 驱动也得是异步的(如asyncpg、aiomysql),否则await一行就卡死 -
CPU 密集 + I/O 混合(如图片缩放+上传、日志解析+上报):主线程用
asyncio处理网络,CPU 工作用loop.run_in_executor()丢给concurrent.futures.ProcessPoolExecutor;别用ThreadPoolExecutor,GIL 让它白忙 -
强一致性状态管理(如计费、库存扣减):协程间不共享状态,
global counter; counter += 1是竞态灾难。要么用asyncio.Lock,要么把临界区逻辑下沉到原子 DB 操作(如UPDATE ... SET stock = stock - 1 WHERE stock >= 1)
最容易被忽略的一点:异步不是银弹。你写的每个 await 点,都是潜在的调度让出点——如果某次 await db.fetch() 因慢查询卡住 5 秒,整个事件循环就停 5 秒。监控得盯紧协程平均延迟和事件循环滞留时间(asyncio.base_events._run_once 耗时),而不是只看 CPU 使用率。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










