AIGC标识 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 都行。

posted @ 2026-10-22 08:58  小小张说故事  阅读(22)  评论(0)    收藏  举报