python-gil

多线程 Python 服务增加线程却不增加吞吐,CPU 只跑满一个核。

现象

CPU 密集接口在并发升高后延迟不降、QPS 不涨;top -H 显示只有一个线程接近 100%,其余线程大量时间在等待。常见于把纯 Python 计算塞进 ThreadPoolExecutor 或 threading 的服务。

快速定位

先确认是 CPU 密集还是 I/O 密集,再看线程都卡在哪里:

python -VV
python -c "import sys, sysconfig; print(getattr(sys, '_is_gil_enabled', lambda: 'n/a')()); print(sysconfig.get_config_var('Py_GIL_DISABLED'))"
top -H -p <PID>
ps -L -o pid,tid,pcpu,stat,comm -p <PID> | sort -k3 -nr | head
py-spy top --pid <PID>
py-spy dump --pid <PID> --gil

py-spy dump --gil 能看到线程等待 GIL 的状态;若大量栈停在 take_gil 或解释器求值循环,基本可以确认 GIL 争用。再看系统调用占比:

timeout 10 strace -f -c -p <PID>
perf top -p <PID>

易误判:单核打满不一定是 GIL —— 也可能是代码本来就是单线程路径、全局锁或某个不释放 GIL 的 C 调用。反过来,3.13 free-threaded 构建在导入未声明支持自由线程的 C 扩展时会自动重新启用 GIL,sys._is_gil_enabled() 返回 True 就说明仍在 GIL 下运行。

常见根因

  1. CPU 密集的纯 Python 代码用线程并行,GIL 把字节码执行串行化。
  2. C 扩展在长耗时调用期间不释放 GIL(部分加密、压缩、正则和自研扩展)。
  3. 线程数过多导致 GIL 抢占和上下文切换开销,吞吐反而下降。
  4. 即使没有 GIL,也存在全局锁/热点锁,把并发压成串行。
  5. free-threaded 3.13 环境加载了不兼容扩展,GIL 被自动重新启用。
  6. fork 与多线程混用,子进程只剩调用线程,继承的锁状态可能死锁。
  7. C 扩展频繁回调 Python,反复进出 GIL,放大切换成本。
  8. 误把 I/O 密集任务当成 CPU 密集任务,线程池大小和超时设置不合理。

修复

相关命令

读完自测

  1. 为什么给纯 Python 计算加线程不能利用多核?
  2. py-spy dump --gil 显示大量线程等待 GIL,下一步该改什么?
  3. free-threaded Python 3.13 为什么可能仍然跑在 GIL 下?
答案
  1. CPython 同一时刻只允许一个线程执行字节码,线程只在 I/O 或显式释放 GIL 时切换,CPU 密集任务无法并行。
  2. 先区分任务类型:CPU 密集改多进程/原生扩展,I/O 密集保留线程或改 asyncio;同时排查不释放 GIL 的 C 调用。
  3. 导入未声明支持 free-threading 的 C 扩展时,解释器会自动重新启用 GIL;需确认扩展兼容性。