Python GIL 对多线程的影响

9次阅读

python线程跑CPU密集任务几乎不提速,因GIL强制同一时刻仅一个线程执行字节码,导致多线程实际串行执行,且有线程切换与GIL争抢开销。

Python GIL 对多线程的影响

Python 多线程跑 CPU 密集任务为什么几乎不提速

因为 GIL(Global Interpreter Lock)强制同一时刻只有一个线程执行 Python 字节码。即使你开了 8 个 Threading.Thread,它们在 CPU 密集场景下仍会排队等待 GIL,实际是串行执行。

典型表现:用多线程计算斐波那契、矩阵乘法或循环累加,耗时几乎等于单线程——甚至更慢(线程切换开销+GIL争抢)。

  • 纯 Python 循环、数学运算、字符串处理等都受 GIL 限制
  • time.sleep()socket.recv()file.read() 等 I/O 操作会主动释放 GIL,此时其他线程可运行
  • C 扩展(如 numpy 的大部分数组运算)通常在内部释放 GIL,所以多线程调用 np.dot 可能真正并行

什么时候该用 threading 而不是 multiprocessing

当任务本质是 I/O 密集型,且需要共享内存状态(比如共用一个字典缓存、一个数据库连接池),threading 更轻量、通信无序列化成本。

常见适用场景:

立即学习Python免费学习笔记(深入)”;

  • 并发发起 http 请求(requests.get 期间 GIL 已释放)
  • 监听多个 socket 连接(selectpoll 阻塞时让出 GIL)
  • 定时轮询文件变化或队列消息(queue.Queue 是线程安全的)

注意:threading全局变量可直接读写,但需用 threading.Lock 保护临界区;而 multiprocessing 中进程间默认不共享内存,改用 Managershared_memory 代价更高。

如何验证当前线程是否持有 GIL

没法直接“读取”GIL 状态,但可通过行为间接判断:在纯计算函数中插入 time.sleep(0),若性能显著下降,说明原代码原本在持续占用 GIL;反之,如果加了 sleep 后总耗时不变,可能本就频繁让出 GIL(比如调用了带释放逻辑的 C 函数)。

更可靠的方式是用系统工具观察 CPU 利用率:

  • 单线程 CPU 密集任务:1 个核心跑满(100%)
  • 多线程 CPU 密集任务:仍是 1 个核心跑满,其余核心空闲
  • 多线程 I/O 密集任务:多个核心活跃(因线程在等待 I/O 时被调度到不同核)

linux 下可用 htop 查看 per-thread CPU%,macOS 可用 Activity Monitor 切换到 “Threads” 视图。

绕不开 GIL 时的实用替代方案

真要并行 CPU 工作,multiprocessing 是最直接的选择,但它有启动开销和数据序列化成本。对小任务不划算,对大计算才值得。

其他可行路径:

  • concurrent.futures.ProcessPoolExecutor 替代 ThreadPoolExecutor接口几乎一致,只需改一行初始化代码
  • 把计算密集部分封装成独立脚本,用 subprocess.run 启动,避免解释器级耦合
  • 换语言:Cython 编译关键循环并显式释放 GIL(用 with nogil:),或用 rust 写扩展(通过 pyo3
  • 异步 I/O(asyncio)处理高并发网络请求——它不解决 CPU 并行,但比多线程更省内存、更高吞吐

GIL 不是 bug,是 CPython 实现内存管理(引用计数)的取舍。理解它何时生效、何时失效,比试图“干掉它”更重要。很多所谓“GIL 问题”,其实是选错了并发模型。

text=ZqhQzanResources