- cpython/Python/ceval.c
- cpython/Python/ceval_gil.h
- cpython/Include/internal/pycore_gil.h
这是 Global Interpreter Lock 的定义(我来翻译一下)
在 CPython 中, 全局解释器锁, 或者 GIL, 是一把互斥锁, 这把互斥锁被用来保护 python 对象, 防止多个线程同时执行 python 字节码. 这把锁是有存在的必要的, 主要原因是 CPython 的内存管理机制在实现的时候是非线程安全的(因为GIL的存在, 很多第三方的扩展和功能在写的时候都深度的依赖这把锁, 更加加深了对 GIL 的依赖)
通俗的来讲, tick 是一个计数器, 表示当前线程在释放 gil 之前连续的执行了多少个字节码(实际上有部分执行较快的字节码并不会被计入计数器)
如果当前的线程正在执行一个 CPU 密集型的任务, 它会在 tick 计数器到达 100 之后就释放 gil, 给其他线程一个获得 gil 的机会
如果当前的线程正在执行一个 IO 密集型的任务, 你执行 sleep/recv/send(...etc)
这些会阻塞的系统调用时, 即使 tick 计数器的值还没到 100, gil 也会被主动地释放
你可以调用 sys.setcheckinterval()
这个函数把 tick 计数器的值从默认的 100 改为其他的值
(图片来自 Understanding the Python GIL(youtube))
因为 tick 并不是以时间为基准计数, 而是以 opcode 个数为基准的计数, 有一些 opcode 代码复杂耗时长, 一些耗时短, 进而导致同样的 100 个 tick, 一些线程的执行时间总是执行的比另一些线程长
在多核机器上, 如果两个线程都在执行 CPU 密集型的任务, 操作系统有可能让这两个线程在不同的核心上运行, 也许会出现以下的情况, 当一个拥有了 gil 的线程在一个核心上执行 100 次 tick 的过程中, 在另一个核心上运行的线程频繁的进行抢占 gil, 抢占失败的循环, 导致 CPU 空转影响性能
当前的实现完全的把任务(线程)调度交给了操作系统, 哪个线程会抢到锁被唤醒, 哪个线程抢不到锁被阻塞, 程序员是无法控制的, 这也就导致了以下情况发生的概率, 想象一下一个处理 IO 密集型的线程已经收到了 IO 的信号, 但是需要等待另一个线程释放 gil 才能去处理, 在等待的过程中, 另一个线程执行满了 tick 次数, 释放了 gil, 但是之后自己又抢到了自己释放的 gil, 导致前面的 IO 密集型的线程又需要至少多等待 100 个 tick 才能处理已经接收到的信号, 有可能在信号丢失前都无法及时处理(实际上, 自己主动触发操作系统进行任务调度的线程会比被操作系统强制触发任务调度的线程在执行队列里有更高的优先级, 程序员可以让 IO 密集型任务尽快的进入等待状态, 主动触发任务调度, 提高这个线程的优先级)(详情参考操作系统相关资料)
(图片来自 Understanding the Python GIL(youtube))
由于上面列出的多核机器下可能导致的性能等方面的影响, gil 的实现在 python3.2 之后进行了一些优化
如果当前只有一个线程, 那么这个线程会永远执行下去, 无需检查并释放 gil
如果当前有不止一个线程, 当前等待 gil 的线程在超过一定时间的等待后, 会把全局变量 gil_drop_request 的值设置为 1, 之后继续等待相同的时间, 这时拥有 gil 的线程看到了 gil_drop_request 变为 1, 就会主动释放 gil 并通过 condition variable 通知到在等待中的线程, 第一个被唤醒的等待中的线程会抢到 gil 并执行相应的任务
(图片来自 Understanding the Python GIL(youtube))
注意, 把 gil_drop_request 设置为 1 的线程不一定是抢到 gil 的线程
如果当前的线程正在等待 gil, 并且在等待的过程中 gil 被释放并且被其他的线程获得了, 那么当前的线程过了等待时间之后, 需要重新等待, 把 gil_drop_request 设置为 1 并再次进入等待的循环
(图片来自 Understanding the Python GIL(youtube))
如果你对 gil 的详细介绍感兴趣, 请参考 Understanding the Python GIL(article)
python 解释器本质上是一个 C 程序, 所有的可执行的 C 程序都有 main
函数的入口, python 解释器也不例外
你可以在 cpython/Modules/main.c
找到和 main
函数相关的部分, 通过这部分函数你可以发现, 在执行 main loop
之前, 解释器做了很多相关变量的初始化, 其中就包括创建 _gil_runtime_state
和初始化里面的值
./python.exe
>>> import sys
>>> sys.getswitchinterval()
0.005
interval 是线程在设置 gil_drop_request
这个变量之前需要等待的时长(单位微秒), 5000 微秒 等价于 0.005 秒
在 C 里面是用 微秒 为单位存储, 在 python 解释器中以秒来表示
last_holder 存放了最后一个持有 gil 的线程的 C 中对应的 PyThreadState 结构的指针地址, 通过这个值我们可以知道当前线程释放了 gil 后, 是否有其他线程获得了 gil(可以采取措施避免被自己重新获得)
locked 的类型为 _Py_atomic_int, 值 -1 表示还未初始化, 0 表示当前的 gil 处于释放状态, 1 表示某个线程已经占用了 gil, 这个值的类型设置为原子类型之后在 ceval.c
就可以不加锁的对这个值进行读取
/* cpython/Python/ceval_gil.h */
static void take_gil(PyThreadState *tstate)
{
/* 忽略 */
/* 这个位置已经获得了 GIL */
_Py_atomic_store_relaxed(&_PyRuntime.ceval.gil.locked, 1);
_Py_ANNOTATE_RWLOCK_ACQUIRED(&_PyRuntime.ceval.gil.locked, /*is_write=*/1);
if (tstate != (PyThreadState*)_Py_atomic_load_relaxed(
&_PyRuntime.ceval.gil.last_holder))
{
_Py_atomic_store_relaxed(&_PyRuntime.ceval.gil.last_holder,
(uintptr_t)tstate);
++_PyRuntime.ceval.gil.switch_number;
}
/* 忽略 */
}
static void drop_gil(PyThreadState *tstate)
{
/* 忽略 */
if (tstate != NULL) {
_Py_atomic_store_relaxed(&_PyRuntime.ceval.gil.last_holder,
(uintptr_t)tstate);
}
MUTEX_LOCK(_PyRuntime.ceval.gil.mutex);
_Py_ANNOTATE_RWLOCK_RELEASED(&_PyRuntime.ceval.gil.locked, /*is_write=*/1);
_Py_atomic_store_relaxed(&_PyRuntime.ceval.gil.locked, 0);
/* 忽略 */
}
switch_number 是一个计数器, 表示从解释器运行到现在, gil 总共被释放获得多少次
在函数 take_gil
中使用到
static void take_gil(PyThreadState *tstate)
{
/* 忽略 */
while (_Py_atomic_load_relaxed(&_PyRuntime.ceval.gil.locked)) {
/* 只要 gil 是锁住的状态, 进入这个循环 */
int timed_out = 0;
unsigned long saved_switchnum;
saved_switchnum = _PyRuntime.ceval.gil.switch_number;
/* 释放 gil.mutex, 并在以下两种条件下唤醒
1. 等待 INTERVAL 微秒(默认 5000)
2. 还没有等待到 5000 微秒但是收到了 gil.cond 的信号
*/
COND_TIMED_WAIT(_PyRuntime.ceval.gil.cond, _PyRuntime.ceval.gil.mutex,
INTERVAL, timed_out);
/* 当前持有 gil.mutex 这把互斥锁 */
if (timed_out &&
_Py_atomic_load_relaxed(&_PyRuntime.ceval.gil.locked) &&
_PyRuntime.ceval.gil.switch_number == saved_switchnum) {
/* 如果超过了等待时间, 并且这段等待时间里 gil 的持有者没有变更过, 则尝试让当前持有 gil 的线程进行释放gil
把 gil_drop_request 值设为 1, 持有锁的线程看到这个值的时候, 会尝试放弃 gil */
SET_GIL_DROP_REQUEST();
}
/* 继续回到 while 循环, 检查 gil 是否为锁住状态 */
}
/* 忽略 */
}
mutex 是一把互斥锁, 用来保护 locked
, last_holder
, switch_number
还有 _gil_runtime_state
中的其他变量
cond 是一个 condition variable, 和 mutex 结合起来一起使用, 当前线程释放 gil 时用来给其他等待中的线程发送信号
switch_cond 是另一个 condition variable, 和 switch_mutex 结合起来可以用来保证释放后重新获得 gil 的线程不是同一个前面释放 gil 的线程, 避免 gil 切换时线程未切换浪费 cpu 时间
这个功能如果编译时未定义 FORCE_SWITCHING
则不开启
static void drop_gil(PyThreadState *tstate)
{
/* 忽略 */
#ifdef FORCE_SWITCHING
if (_Py_atomic_load_relaxed(&_PyRuntime.ceval.gil_drop_request) &&
tstate != NULL)
{
/* 如果 gil_drop_request 已经设置了并且 tstate 不为空 */
/* 锁住 switch_mutex 这把互斥锁 */
MUTEX_LOCK(_PyRuntime.ceval.gil.switch_mutex);
if (((PyThreadState*)_Py_atomic_load_relaxed(
&_PyRuntime.ceval.gil.last_holder)
) == tstate)
{
/* 如果 last_holder 是当前线程, 释放 switch_mutex 这把互斥锁, 等待 switch_cond 这个条件变量的信号 */
RESET_GIL_DROP_REQUEST();
/* 注意, 如果 COND_WAIT 不在互斥锁释放后原子的启动,
另一个线程有可能会在这中间拿到 gil 并释放,
'并且重置这个条件变量, 这个过程发生在了 COND_WAIT 之前 */
COND_WAIT(_PyRuntime.ceval.gil.switch_cond,
_PyRuntime.ceval.gil.switch_mutex);
}
MUTEX_UNLOCK(_PyRuntime.ceval.gil.switch_mutex);
}
#endif
}
cpython/Python/ceval.c
中的 main_loop
是一个很大的 for loop
, 并且其中含有一个很大的 switch statement
这个很大的 for loop
会按顺序逐个的加载 opcode, 并委派给中间很大的 switch statement
去进行执行, switch statement
会根据不同的 opcode 跳转到不同的位置执行
for loop
在开始位置会检查 gil_drop_request
变量, 必要的时候会释放 gil
不是所有的 opcode 执行之前都会检查 gil_drop_request
的, 有一些 opcode 结束时的代码为 FAST_DISPATCH()
, 这部分 opcode 会直接跳转到下一个 opcode 对应的代码的部分进行执行
而另一些 DISPATCH()
结尾的作用和 continue
类似, 会跳转到 for loop
顶端, 重新检测 gil_drop_request
, 必要时释放 gil
/* cpython/Python/ceval.c */
main_loop:
for (;;) {
/* 忽略 */
if (_Py_atomic_load_relaxed(&_PyRuntime.ceval.eval_breaker)) {
opcode = _Py_OPCODE(*next_instr);
if (opcode == SETUP_FINALLY ||
opcode == SETUP_WITH ||
opcode == BEFORE_ASYNC_WITH ||
opcode == YIELD_FROM) {
/* 跳过 gil 部分, 直接跳转到 switch 部分 */
goto fast_next_opcode;
}
/* 忽略 */
if (_Py_atomic_load_relaxed(
&_PyRuntime.ceval.gil_drop_request))
{
/* 如果 gil_drop_request 被其他线程设置为 1 */
/* 给其他线程一个获得 gil 的机会 */
if (PyThreadState_Swap(NULL) != tstate)
Py_FatalError("ceval: tstate mix-up");
drop_gil(tstate);
/* 其他线程现在在运行中 */
take_gil(tstate);
/* 检查是否需要退出 */
if (_Py_IsFinalizing() &&
!_Py_CURRENTLY_FINALIZING(tstate))
{
drop_gil(tstate);
PyThread_exit_thread();
}
if (PyThreadState_Swap(tstate) != NULL)
Py_FatalError("ceval: orphan tstate");
}
/* 忽略 */
}
fast_next_opcode:
/* 忽略 */
switch (opcode) {
case TARGET(NOP): {
FAST_DISPATCH();
}
/* 忽略 */
case TARGET(UNARY_POSITIVE): {
PyObject *value = TOP();
PyObject *res = PyNumber_Positive(value);
Py_DECREF(value);
SET_TOP(res);
if (res == NULL)
goto error;
DISPATCH();
}
/* 忽略 */
}
/* 忽略 */
}