协程切换在用户态、线程切换需进内核:协程仅保存局部变量和执行位置,切换耗时纳秒级;线程需保存寄存器、栈指针等完整上下文,耗时微秒级且随数量非线性增长。

协程切换在用户态,线程切换必须进内核
协程每次 await 挂起,只保存局部变量和执行位置(本质是生成器状态),事件循环在用户态直接跳转到下一个就绪协程;而线程调用 socket.recv() 或 time.sleep() 时,会立刻陷入内核态等待,操作系统得保存寄存器、栈指针、页表等完整上下文——这个过程耗时从数百纳秒到微秒级,且随线程数增长非线性飙升。
实操建议:
- 观察现象:用 ps -o pid,vsz,comm -p $(pidof python) 对比内存占用最直观
- 1000个HTTP请求,aiohttp.ClientSession.get() + asyncio.gather() 常驻内存约20–30MB
- 同量请求用 requests.get() + threading.Thread,内存通常超200MB,甚至突破8GB(Linux默认线程栈8MB × 1000)
协程天然规避竞态,线程必须显式加锁
协程跑在单线程里,任意时刻只有一个在执行,counter += 1 这种操作只要不被 await 打断,就是原子的;多线程则天然共享全局变量,但该操作实际是读-改-写三步,不加 threading.Lock 必然出错。
但注意:
- 协程间不自动共享状态。误写 global counter; counter += 1 看似没报错,其实是竞态灾难
- 需要跨 Task 计数/缓存,用 asyncio.Lock 包裹临界区,别碰 threading.Lock(它会阻塞整个事件循环)
- 复杂状态逻辑优先走 asyncio.Queue,生产者-消费者模型比锁更清晰、更易测
协程无GIL争抢,但也不等于“自动并行”
asyncio.run() 启动的是单线程事件循环,绑定在主线程,无法利用多核CPU。这点常被忽略:很多人以为“异步=多核并行”,其实 asyncio 只解决 I/O 等待效率,不解决 CPU 计算瓶颈。
常见错误现象:
- 在协程里写 sum(range(10**7)) 或 re.findall(r'.*', big_text),整个事件循环卡死,其他所有协程冻结
- 用 time.sleep(1) 代替 await asyncio.sleep(1),效果等同于上面的 CPU 密集型阻塞
- 实操建议:CPU 密集操作必须扔进线程池——await loop.run_in_executor(None, func, *args)
I/O密集型任务选协程,不是“更高效”,而是资源维度不同
协程能用单线程承载数千并发任务,而多线程一上几百个就可能触发 OSError: can't start new thread 或被 OOM killer 干掉。这不是“更高效”,而是根本不在同一资源维度上竞争。
立即学习“Python免费学习笔记(深入)”;
真正容易被忽略的点:
- 协程调度依赖显式让渡(await),任何依赖执行顺序的逻辑都得显式建模
- await 点就是潜在的状态让渡点,比如 await asyncio.sleep(0) 也会交出控制权
- 如果你混用 threading 和 asyncio(比如在协程里直接调用带锁的同步函数),极易引发死锁或不可预测行为


















