Skip to content

03 · Python 异步与并发 ​

前端对事件循环很熟,但把 JS 那套并发直觉搬到 Python 会翻车:asyncio 是库不是语言特性、有 GIL、多线程不等于并行。本篇讲清 asyncio / GIL / 多进程的边界,以及"什么时候 asyncio 救不了你"。

速览卡 ​

项内容
一句话定位Python 并发三件套的选型与直觉
核心内容asyncio 与 JS 差异、GIL、线程 vs 进程 vs 协程
读完能回答为什么多线程加不了速?批量请求该用 asyncio 还是线程池?
相关板块E01 Python 基础 | N12 IO 模型 | C07 性能与容量

一、JS 直觉为什么失效 ​

维度JSPython
事件循环语言内置,自动跑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 无效
ProcessPoolExecutorCPU 密集并行进程间通信、内存开销
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.allasyncio.gather
awaitawait(语法类似)
setTimeoutasyncio.sleep
Web Worker多进程
Promise.allSettledasyncio.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。


相关文档 ​

本区 ​

网络区(异步的底层) ​

后端区(生产落地) ​