Python 的 GIL 到底是什么?拆开「一次一条字节码」:为什么多线程算数是负优化
结论先行:GIL 不是「Python 慢」的病因,而是「同一进程里多个线程不能同时跑字节码」的一把大锁——CPU 密集型任务上,2 个线程跑 2 份活的实测加速比是 1.00x(156ms 的活,单线程 156ms,双线程 312ms,一点没省);但 I/O 密集型任务上 4 个线程能拿到 1.6x。更反直觉的是:把切换间隔从默认 5ms 调到 0.5ms,双线程反而从 312ms 慢到 425ms——锁本身不贵,抢锁才贵。这篇用 5 组实验把 GIL 的边界画清楚。
1. 先看实测:多线程算数,加速比 1.00x
写一个纯计算的活(200 万次循环累加),三种跑法:
| 跑法 | 耗时 | 加速比 |
|---|---|---|
| 单线程跑 1 份 | 156 ms | — |
| 2 个线程各跑 1 份 | 312 ms | 1.00x |
312ms 恰好是 156 的两倍——两个线程像接力一样把 GIL 传来传去,总时间和串行跑完两份一模一样。这就是 GIL 的标准症状:不是变慢,是白忙。
2. GIL 锁住的是什么?一次一条字节码
关键认知:GIL 锁的不是「一段代码」,而是「解释器执行字节码的资格」。
CPython 的执行循环是:取一条字节码 → 执行 → 取下一条。GIL 保证整个进程里只有持有锁的那个线程能进入这个循环。任何时刻,一个进程里只有一条字节码在跑——哪怕你有 16 核。
那什么时候换人?两条规则:
- 默认每 5 毫秒(
sys.getswitchinterval(),实测默认值 5.0ms):到点后运行的线程评估「要不要让」; - 阻塞操作主动放锁:I/O、
time.sleep、acquire失败——等不起的活,先放锁让别人跑。
3. 为什么 I/O 型任务多线程有用
既然 GIL 锁字节码,多线程是不是全废?不是。实测一个「失败连接」型 I/O 活(每份 5 次连接尝试):
| 跑法 | 耗时 | 加速比 |
|---|---|---|
| 单线程 | 276 ms | — |
| 4 个线程 | 175 ms | 1.6x |
原因:线程在等 socket 的时候不持有 GIL。I/O 等待期间,锁是放开的,其他线程尽可以跑。所以爬虫、Web 服务这类「大部分时间在等」的场景,多线程依然有效——GIL 挡的是计算,不是等待。
这就是为什么 asyncio、多线程、多进程三条路线能并存:等的用 asyncio/线程,算的用进程。
4. 反直觉:切换越勤,反而越慢
既然 5ms 一切换,那把间隔调小、让线程「更公平」不是更好?实测打脸:
| switchinterval | 2 线程各跑 1 份 |
|---|---|
| 5 ms(默认) | 312 ms |
| 0.5 ms | 425 ms |
慢了 36%。原因:GIL 的切换成本不在锁本身,在抢。线程被唤醒后要重新参与竞争,核心数有限时切换越频繁,缓存失效和竞争开销占比越高。默认 5ms 是多年调校的平衡点——不是懒,是试出来的。
顺带解释了另一件事:为什么 sys.setswitchinterval(0.0005) 这类「优化」帖子基本是玄学——你在解决一个不存在的问题(GIL 切换不够快),同时制造一个真问题(抢锁更凶)。
5. 想用满多核?用进程,别跟 GIL 较劲
CPU 密集型任务要并行,正解是 multiprocessing:每个进程一把独立的 GIL,操作系统层面真并行。代价也实在——进程创建/通信开销大,数据要序列化(pickle)传递,小任务反而更慢。
选型表:
| 任务类型 | 推荐 | 原因 |
|---|---|---|
| CPU 密集(计算、编解码、数据处理) | 多进程 / C 扩展 / numpy | 绕开 GIL |
| I/O 密集(网络、磁盘、爬虫) | 多线程 / asyncio | 等待期锁是放开的 |
| 混合型 | 进程池 + 线程池 | 各管一段 |
值得一说的是,C 扩展可以在计算时主动放锁——numpy 的大矩阵运算、Pillow 的图像处理之所以能「看起来并行」,是因为它们在 C 层把 GIL 放了。这也是为什么科学计算栈没人抱怨 GIL。
6. GIL 为什么还没被删?
PEP 703(3.13 的自由线程构建)正在做这件事,但默认构建仍带 GIL。原因很现实:CPython 的性能和内存安全长期建立在「解释器全局状态无需加锁」这个假设上——对象引用计数、内置类型的全局缓存(小整数、intern 字符串)都受益于此。删 GIL 不是删一把锁,是重写整个内存模型,还要保证不倒退单线程性能。
所以现状是:****自由线程是可选构建(python3.13t),默认构建继续带 GIL。你的多线程代码短期内还得按上面那张表选型。
一句话收尾:GIL 的本质是「一个进程一次只跑一条字节码」——CPU 密集型多线程实测加速比 1.00x,I/O 密集能到 1.6x(等待期锁是放开的),把切换间隔调小只会更慢(抢锁比持锁贵)。要并行算,就上进程;要并行等,线程和 asyncio 都行。

浙公网安备 33010602011771号