本文目录导读:
- 核心数据结构:
_PyRuntimeState.gil - 真正阻塞的机制:
PyMutex+ 条件变量 - 核心函数剖析:
take_gil()和drop_gil() - 执行循环中的 GIL 检查:
_PyEval_EvalFrameDefault - 信号与强行切换的细节
- 总结 GIL 实现的三层架构
- 一个代码级的小实验
要深入理解 CPython 中 GIL 的具体实现,最佳路径是从源码入手,结合关键数据结构和执行流程来分析,下面我会带你从 CPython(以 3.12 版本为例,核心机制稳定)的源码角度,解析 GIL 的实现细节。
GIL 的核心目标只有一个:确保同一时刻只有一个线程执行 Python 字节码,它的实现主要围绕 ceval.c 和 pthread(或其他线程库) 展开。
核心数据结构:_PyRuntimeState.gil
GIL 本身不是一个独立的“锁对象”,而是嵌入在 CPython 运行时状态中的一个结构体,关键位置在 Include/internal/pycore_gil.h 和 Python/ceval_gil.h。
// Include/internal/pycore_gil.h (简化)
typedef struct _gil_runtime_state {
// 自旋锁,用于保护 GIL 内部状态(如 last_holder)的快速访问
_Py_atomic_int locked;
// 当前持有 GIL 的线程的 ID (由线程库提供,如 pthread_t)
unsigned long last_holder;
} _PyRuntime_state.gil;
关键点:
locked只是一个原子整数(0/1),不是互斥锁。GIL 真正的阻塞机制不在内部。last_holder用于调试和性能分析,记录谁最后持有锁。
GIL 不是一个“互斥锁”锁住一个资源,而是一个 “线程所有权指示器”,线程通过一个底层条件变量来等待或通知。
真正阻塞的机制:PyMutex + 条件变量
在 ceval_gil.h 中,实现了 GIL 的获取和释放,底层使用了 PyThread_type_lock 这个跨平台的简单锁,但更关键的是内部使用的 struct _gil_cond:
// Python/ceval_gil.c (实际结构)
struct _gil_cond {
// 用于让等待的线程休眠,被释放时唤醒
PyCOND_T cond; // 通常是 pthread_cond_t
// 保护 cond 的互斥锁,防止条件变量的竞争
PyMUTEX_T mut; // 通常是 pthread_mutex_t
// 开关: 是否启用 GIL 超时切换
int switch_interval; // 默认 5ms
// 记录下次强制切换的时间点
_PyTime_t interval_end;
};
为什么需要 mut 和 cond?
因为操作系统级别的锁(pthread_mutex)不适合直接作为 GIL使用,CPython 需要支持:
- 出让主动: 线程自己释放 GIL。
- 抢占被动: 另一个线程可以强制当前持有者放弃 GIL(通过信号或超时)。
核心函数剖析:take_gil() 和 drop_gil()
1 take_gil() — 尝试获取 GIL
这是每个线程执行 Python 代码前必须调用的函数,位于 Python/ceval_gil.c。
流程伪码简化:
static void take_gil(PyThreadState *tstate) {
struct _gil_cond *gil_cond = &_PyRuntime.gil.cond;
int err;
// GIL 未被锁定,尝试原子操作锁住它
if (!_Py_atomic_load(&_PyRuntime.gil.locked)) {
if (/* 比较并交换成功 */) {
// 快速路径: 直接获得 GIL,设置 last_holder
goto done;
}
}
// 慢速路径: 进入循环,等待条件变量
while (1) {
// 进入临界区: 锁住 mut
PyMUTEX_LOCK(&gil_cond->mut);
// 再次检查 GIL 是否已被释放(避免刚解锁又被其他人抢走)
if (/* 检查 locked 并尝试获取 */) {
// 成功获取,退出循环
PyMUTEX_UNLOCK(&gil_cond->mut);
break;
}
// 为什么要进入条件变量等待?
// 因为当前线程需要被阻塞,直到持有者主动释放或被强制切换。
// 设置超时: 每 5ms 唤醒一次,以检查是否需要抢占
if (/* 超时时间未到 */) {
// 阻塞在条件变量上,等待 signal 或广播
err = PyCOND_WAIT(&gil_cond->cond, &gil_cond->mut, timeout);
} else {
// 时间到了,主动尝试抢占!
// 这不是简单的释放,而是通过信号强制当前持有者线程放弃 GIL
PyMUTEX_UNLOCK(&gil_cond->mut);
// 发送信号给持有线程(通常是 SIGUSR1 或通过管道)
_Py_gil_drop_signal();
// 重新进入循环
continue;
}
}
done:
// 记录当前线程为持有者
_PyRuntime.gil.last_holder = tstate->thread_id;
}
关键洞察:
- 获取 GIL 不是一个简单的
pthread_mutex_lock(),它是一个带有 自旋 + 条件变量等待 + 超时重试 的复合操作。 - 慢路径中的 超时(一般为5ms) 是 GIL 切换的粒度,每5ms,等待线程会醒来一次,检查锁是否可用。
2 drop_gil() — 释放 GIL
这是线程执行完一段 Python 字节码后(例如遇到 I/O,或时间片用完)调用的函数。
static void drop_gil(struct _ceval_state *ceval, PyThreadState *tstate) {
struct _gil_cond *gil_cond = &_PyRuntime.gil.cond;
// 1. 原子设置 locked = 0
_Py_atomic_store(&_PyRuntime.gil.locked, 0);
// 2. 通知其他等待的线程: 锁已释放
// 注意: 这里必须在释放 mut 之前 signal,否则可能有竞争
PyCOND_SIGNAL(&gil_cond->cond); // 或者 PyCOND_BROADCAST 唤醒所有
// 3. 清除 last_holder (可选,调试用)
_PyRuntime.gil.last_holder = 0;
}
关键洞察:
- 释放 GIL 不释放
mut锁。cond和mut是分离的。 - 为什么需要条件变量 signal?
因为等待的线程可能阻塞在PyCOND_WAIT上,如果不发 signal,它可能一直睡到超时(5ms),signal 可以使其立即醒来,减少延迟。
执行循环中的 GIL 检查:_PyEval_EvalFrameDefault
在 CPython 的解释器核心循环(Python/ceval.c 中的 _PyEval_EvalFrameDefault)中,每次执行 Python 字节码指令时,会周期性检查是否需要释放/重新获取 GIL。
关键机制:CHECK_EVAL_BREAKER() 和 DROP_GIL()。
// _PyEval_EvalFrameDefault 的简化循环
for (;;) {
// 常规字节码执行...
// 每执行一些指令后,检查是否应该让出 GIL
if (_Py_atomic_load(&ceval->gil_drop_request)) {
// 有线程请求 GIL,或者时间片用完
DROP_GIL(tstate); // 释放 GIL
TAKE_GIL(tstate); // 重新获取 GIL (可能被阻塞在此)
// 注意: 重新获取后,可能已经是另一个线程获得了锁,或者本线程继续运行
}
// 如果发生信号,可能强制休眠
if (/* 信号导致切换 */) {
// 类似处理
}
}
gil_drop_request 标志位:
- 这不是一个简单的定时器中断,它是 一个等待线程在
take_gil()超时后,通过信号(如pthread_kill(SIGUSR1))设置 的全局标志。 - 当前持有线程在检查到这个标志后,会主动释放 GIL。
- 为什么不用信号实现强迫切换?
因为信号不安全(信号处理函数中可以做的事有限),CPython 采用 “设置标志 → 当前线程在安全点检查 → 主动释放” 的协作式模式。
信号与强行切换的细节
当等待线程在 take_gil() 中等待了 5ms 仍未获得 GIL 时,它会发送一个 GILDropRequestSignal (通常是 SIGUSR1)。
当前持有线程会响应这个信号吗?
答案:不一定立即响应。
信号处理函数(在 Python/ceval.c 中)只是设置一个 ceval->gil_drop_request 标志,然后在下次检查点(通常每 30-100 条指令)主动释放。
这带来的影响:GIL 切换的延迟大约在 5ms(等待超时) + 几十条指令的时间,而不是立刻。
GIL 实现的三层架构
| 层级 | 组件 | 作用 |
|---|---|---|
| 原子标志 | gil.locked |
快速检查当前是否被持有 |
| 协同切换 | gil_drop_request + 检查点 |
强制当前线程让出 GIL 的请求 |
| 条件变量+自旋 | gil_cond (mut + cond) |
让等待线程休眠并按时唤醒 |
为什么这么设计?
- 避免忙等待(自旋)浪费 CPU。
- 避免频繁的系统调用(如
pthread_mutex_lock的上下文切换)。 - 保证多线程 I/O 密集程序的响应速度(当等待超过 5ms 时,强制切换)。
- 保证单线程性能(快速路径
if (!locked)几乎是零开销的原子操作)。
一个代码级的小实验
你可以通过下面 Python 代码来观察 GIL 的行为(在 CPython 源码中可以用 gdb 断点 take_gil 和 drop_gil):
import threading
import time
def busy():
while True:
pass # CPU 密集型,会阻塞 GIL
t = threading.Thread(target=busy, daemon=True)
t.start()
time.sleep(1) # 主线程会被阻塞在 GIL 之外
print("Hello") # 可能看到较明显的延迟
在 CPython 源码级别,主线程会循环在 take_gil() 中的条件变量等待,直到被 busy 线程的 drop_gil 唤醒,或者 5ms 超时后强制发送信号。
通过上述分析,你可以理解 GIL 本质上是一个 合作式的时间片轮转机制 + 基于条件变量的等待队列,而不是一个简单的互斥锁,它的设计平衡了单线程性能与多线程公平性。
标签: CPython源码