03 · Python 异步与并发
前端对事件循环很熟,但把 JS 那套并发直觉搬到 Python 会翻车:
asyncio是库不是语言特性、有 GIL、多线程不等于并行。本篇讲清 asyncio / GIL / 多进程的边界,以及"什么时候 asyncio 救不了你"。
速览卡
| 项 | 内容 |
|---|---|
| 一句话定位 | Python 并发三件套的选型与直觉 |
| 核心内容 | asyncio 与 JS 差异、GIL、线程 vs 进程 vs 协程 |
| 读完能回答 | 为什么多线程加不了速?批量请求该用 asyncio 还是线程池? |
| 相关板块 | E01 Python 基础 | N12 IO 模型 | C07 性能与容量 |
一、JS 直觉为什么失效
| 维度 | JS | Python |
|---|---|---|
| 事件循环 | 语言内置,自动跑 | asyncio 是库,要显式起 |
| 顶层 await | 支持 | 脚本里要 asyncio.run() |
| 并发单元 | 协程是默认 | 协程只是"可挂起的函数",得手动调度 |
| 线程 | 没有真正的多线程共享 | 有线程,但受 GIL 限制 |
最大的坑:在 async 函数里调用同步阻塞代码,事件循环会卡死,因为没有自动把阻塞 IO 交给别的线程。
二、asyncio 的正确用法
python
import asyncio
async def fetch(x):
await asyncio.sleep(0.1) # 必须是"可等待"的 IO
return x
async def main():
results = await asyncio.gather(fetch(1), fetch(2), fetch(3))
print(results)
asyncio.run(main())适用:高并发 IO(批量调 LLM、爬虫、大量网络请求)。 不适用:CPU 密集计算、同步阻塞库(除非交给线程池 run_in_executor)。
三、GIL:为什么多线程不等于并行
text
GIL:同一进程内,任意时刻只有一个线程在执行 Python 字节码| 密集类型 | 多线程 | 多进程 |
|---|---|---|
| CPU 密集 | 不加倍(GIL 挡着) | ✅ 真并行 |
| IO 密集 | ✅(等待时不占 GIL) | 可以但重 |
结论:IO 密集用 asyncio 或线程;CPU 密集用多进程(
ProcessPoolExecutor/multiprocessing)。
四、三件套选型
| 方案 | 解决什么 | 代价 |
|---|---|---|
asyncio | 高并发 IO | 心智成本、阻塞调用要小心 |
ThreadPoolExecutor | 同步库的并发 | GIL 下 CPU 无效 |
ProcessPoolExecutor | CPU 密集并行 | 进程间通信、内存开销 |
python
from concurrent.futures import ProcessPoolExecutor
def heavy(x): # CPU 密集
return sum(range(x))
with ProcessPoolExecutor() as ex:
results = list(ex.map(heavy, [10**6, 10**6]))五、前端对照表
| 前端概念 | Python 对应 |
|---|---|
Promise.all | asyncio.gather |
await | await(语法类似) |
setTimeout | asyncio.sleep |
| Web Worker | 多进程 |
Promise.allSettled | asyncio.gather(return_exceptions=True) |
六、常见踩坑
| 坑 | 后果 | 解法 |
|---|---|---|
async 里调 requests | 事件循环卡死 | 用 httpx/aiohttp |
asyncio.sleep 写普通 sleep | 卡死 | 只 await 可等待对象 |
| CPU 密集却用 asyncio | 白等 | 换多进程 |
忘记 await 调用 | 协程没执行 | 检查 lint / 用 asyncio.run 收口 |
七、自检清单
- [ ] 能说清 asyncio 与 JS 事件循环的三个差异
- [ ] 知道 GIL 为什么限制 CPU 并行
- [ ] 能按密集类型选择 asyncio / 线程 / 进程
- [ ] 知道 async 里调同步阻塞代码会卡死循环
- [ ] 能写出
asyncio.gather与ProcessPoolExecutor的最小示例
八、常见误区
| 误区 | 事实 |
|---|---|
| "Python 和 JS 一样自动异步" | asyncio 是库,要显式驱动 |
| "多线程一定加速" | CPU 密集被 GIL 挡住 |
| "asyncio 万能" | 同步阻塞库照样卡死事件循环 |
| "并发 = 并行" | 协程是并发复用,多进程才是并行 |
九、动手练习
练习 A:对比三种方案
写一个"模拟 IO"任务,分别用串行、asyncio、线程池跑 100 次,记录耗时并解释差异。
练习 B:CPU 密集对比
用 sum(range(n)) 跑大数,对比多线程与多进程耗时,验证 GIL 的结论。
练习 C:找出卡死
在 async 函数里故意用同步 time.sleep,运行观察事件循环卡死,再改成 await asyncio.sleep。
相关文档
本区
- 01 · Python 语言基础与 TS 对照 —— asyncio 与事件循环的差异
- 02 · Python 工程化 —— 并发代码的质量保障
网络区(异步的底层)
- 12 · 网络编程 IO 模型 —— 事件循环的通用原理
后端区(生产落地)
- 06 · 异步与消息:消息队列/事件驱动 —— 异步在服务端的形态
- 07 · 服务端性能与容量:连接池/线程模型 —— 并发容量设计