Global Interpreter Lock — 为什么你的多线程代码跑不满 CPU?
① 兼容性地狱:无数 C 扩展库(NumPy、Pandas、PyTorch...)都假设 GIL 存在,去掉 GIL 意味着它们全部要重写。
② 单线程性能下降:细粒度锁会让单线程代码变慢(有些测试显示慢 30%+)。
③ Python 3.13 开始实验性支持 no-GIL:需要在编译时开启 --disable-gil,还不能用于生产环境。
④ PyPy、Jython、IronPython 等其它实现没有 GIL,但它们也不是 CPython。
Python 的 threading 是真正的 OS 线程(POSIX Thread / Windows Thread),
不是绿色线程或协程模拟。它们可以被操作系统调度到不同 CPU 核心上——但问题在于:
同一时刻只有一个线程能执行 Python 字节码。
所有核心都在等同一把锁,多线程反而因为上下文切换而更慢
I/O 等待时会释放 GIL,其他线程可以继续执行
time.sleep())时,GIL 会被释放,此时其他线程可以获取 GIL 并执行。
# CPU 密集型:多线程无法加速(受 GIL 限制) def cpu_bound(): for i in range(10**8): _ = i * i # I/O 密集型:多线程可以大幅加速 def io_bound(): import time time.sleep(1) # 释放 GIL!其他线程可以运行 # 网络 I/O:requests.get() 内部是 C 实现的, # 在等待响应时会释放 GIL def fetch_url(url): import requests resp = requests.get(url) # GIL 在此处释放 return len(resp.text)
| 释放场景 | 触发条件 | 说明 |
|---|---|---|
| I/O 操作 | read / write / send / recv 等系统调用 | 在 I/O 等待前主动释放 GIL,I/O 完成后重新获取 |
| time.sleep() | 调用 time.sleep(n) | 线程休眠时释放 GIL,定时器到期后重新竞争 |
| 字节码超时 | 默认每 5ms(Python 3.2+)或 15ms(旧版) | 解释器强制当前线程释放 GIL,让其他线程有机会运行 |
| C 扩展主动释放 | C 代码中调用 Py_BEGIN_ALLOW_THREADS | NumPy、Pillow 等 C 扩展在计算时释放 GIL |
| 长时间计算 | 纯 Python 循环(无 I/O) | ⚠️ GIL 不会释放!会一直占用直到超时 |
/* * CPython 3.x 的 GIL 实现(ceval.c / gil.c) * 核心数据结构(简化): * * gil_mutex: 保护 GIL 的互斥锁 * gil_cond: 条件变量,用于线程间通知 * gil_timeout: 当前持有 GIL 的线程最长持有时间(~5ms) * drop_request: 其他线程请求当前线程释放 GIL 的标志 */ // 线程获取 GIL 的简化逻辑 take_gil(PyThreadState *tstate) { wait_for_gil(); // 等待 GIL 可用 // 获取成功,设置定时器 set_gil_timeout(5_MS); } // 字节码执行主循环(ceval.c 核心) PyEval_EvalFrameDefault(...) { for (;;) { // 检查是否该释放 GIL 了(超时或 drop_request) if (gil_must_exit()) { drop_gil(); // 释放 GIL take_gil(tstate); // 重新竞争 GIL } // 执行一条字节码指令 execute_opcode(opcode); } } // C 扩展如何主动释放 GIL(NumPy 等使用此宏) // Py_BEGIN_ALLOW_THREADS 展开为: // PyThreadState *_save; \ // _save = PyEval_SaveThread(); // // Py_END_ALLOW_THREADS 展开为: // PyEval_RestoreThread(_save);
| 方案 | 原理 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| multiprocessing | 每个进程有独立的 GIL | CPU 密集型 | 真正多核并行,绕过 GIL | 内存开销大,进程间通信复杂 |
| concurrent.futures | 封装了 multiprocessing/threading | 通用 | API 简洁,推荐首选 | 本质同上 |
| C 扩展(Cython/CFFI) | C 代码释放 GIL 后并行计算 | 高性能计算 | 极高性能,NumPy 就是这么做的 | 需要写 C/Cython 代码 |
| 无 GIL 的 Python | Python 3.13+ --disable-gil | 未来 | 原生支持,无需改代码 | 实验性,兼容性未验证 |
import multiprocessing as mp import time # 计算密集型任务 def heavy_compute(n): return sum(i * i for i in range(n)) # ---- 单进程基线 ---- def single_process(): t0 = time.time() results = [] for i in range(4): results.append(heavy_compute(10**7)) print(f"单进程: {time.time() - t0:.2f}s") # ---- 多进程(真正并行)---- def multi_process(): t0 = time.time() with mp.Pool(processes=4) as pool: results = pool.map( heavy_compute, [10**7] * 4 ) print(f"多进程(4核): {time.time() - t0:.2f}s") # 输出(4核机器上): # 单进程: ~3.2s # 多进程: ~0.9s ← 接近 4x 加速! if __name__ == "__main__": # Windows 必须加这个保护! # 否则子进程会递归 import,导致无限 fork single_process() multi_process()
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor import time def compute(x): return sum(i*i for i in range(x)) # CPU 密集型 → ProcessPoolExecutor(多进程,绕过 GIL) with ProcessPoolExecutor(max_workers=4) as exe: results = list(exe.map(compute, [10**7] * 4)) # I/O 密集型 → ThreadPoolExecutor(轻量,受 GIL 但 I/O 时会释放) import requests def fetch(url): return requests.get(url).status_code with ThreadPoolExecutor(max_workers=20) as exe: urls = ["https://www.baidu.com"] * 20 results = list(exe.map(fetch, urls))
# 在安装 Cython 后,可以用 nogil 块释放 GIL # 编译后,这段 C 代码可以真正并行 import numpy as np cimport numpy as np from cython import nogil, parallel def parallel_sum(np.ndarray[np.float64_t, ndim=1] arr): cdef double total = 0.0 cdef int i, n = len(arr) # nogil: 在 C 层面释放 GIL,真正并行! with nogil, parallel.prange(n, schedule='static') as i: for i in range(n): total += arr[i] return total
Python/gil.c 和 Python/ceval.c 中。
CPython 源码根目录/
├── Python/
│ ├── gil.c ← GIL 核心实现(获取/释放/超时逻辑)
│ ├── ceval.c ← 字节码执行主循环(解释器核心)
│ ├── thread_pthread.h ← POSIX 线程实现(Linux/macOS)
│ └── thread_nt.h ← Windows 线程实现
├── Include/
│ └── cpython/
│ └── pyatomic.h ← 原子操作(用于无锁计数)
└── Modules/
└── _threadmodule.c ← threading 模块的 C 实现
// CPython 3.12+ 的 GIL 实现(简化版) // 实际源码:https://github.com/python/cpython/blob/main/Python/gil.c // 每个线程的状态 typedef struct { int gil_held; // 是否持有 GIL int gil_locked; // GIL 锁计数(可重入) double last_tick; // 上次获取 GIL 的时间戳 } PyThreadState; // GIL 全局状态 typedef struct { pthread_mutex_t mutex; // POSIX 互斥锁 pthread_cond_t cond; // 条件变量(用于线程等待/唤醒) int pending_events; // 待处理事件计数 int gil_drop_request; // 其他线程请求释放 GIL struct timespec last_switch; // 上次切换时间 } GILState; // ========================================== // 获取 GIL(线程启动时或恢复执行时调用) // ========================================== void take_gil(PyThreadState *tstate) { // 1. 加锁(原子操作) pthread_mutex_lock(&gil.mutex); // 2. 等待 GIL 可用(自旋 + 条件变量) while (gil.gil_held) { // 等待 gil_cond 信号(由 drop_gil 发出) pthread_cond_wait(&gil.cond, &gil.mutex); } // 3. 标记自己持有 GIL gil.gil_held = 1; tstate->gil_held = 1; // 4. 记录时间戳(用于超时判断) clock_gettime(CLOCK_MONOTONIC, &gil.last_switch); pthread_mutex_unlock(&gil.mutex); } // ========================================== // 释放 GIL(I/O 前、超时、sleep 时调用) // ========================================== void drop_gil(PyThreadState *tstate) { pthread_mutex_lock(&gil.mutex); // 1. 标记 GIL 空闲 gil.gil_held = 0; tstate->gil_held = 0; // 2. 唤醒一个等待 GIL 的线程 pthread_cond_signal(&gil.cond); pthread_mutex_unlock(&gil.mutex); } // ========================================== // 检查是否该释放 GIL(在 ceval.c 的字节码循环中调用) // ========================================== int _gil_check(PyThreadState *tstate) { struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now); // 计算持有 GIL 的时间 double held_ms = (now.tv_sec - gil.last_switch.tv_sec) * 1000.0 + (now.tv_nsec - gil.last_switch.tv_nsec) / 1e6; // 超过 5ms,请求释放(Python 3.2+ 的改进版 GIL) if (held_ms > 5.0) { gil.gil_drop_request = 1; return 1; // 该释放了 } return 0; }
pthread_cond_timedwait 让等待线程可以在超时后强制请求 GIL。
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| CPU 密集型(计算、图像处理) | multiprocessing | 每个进程独立 GIL,真正多核并行 |
| I/O 密集型(网络、文件) | threading / asyncio | I/O 时释放 GIL,单进程足够 |
| 高性能计算(NumPy 级别) | C 扩展 / Cython nogil | C 层面释放 GIL,真正并行计算 |
| 混合场景 | concurrent.futures | 统一 API,按需选择 ProcessPool 或 ThreadPool |