编程学习网 > 编程语言 > Python > 为什么 Python 的 GIL 问题一直让人诟病,Python 社区却不解决?
2026
11-02

为什么 Python 的 GIL 问题一直让人诟病,Python 社区却不解决?


假设一个用 C 写的 Python 扩展,要从列表里取出第一个对象,留着稍后使用:

PyObject *item = PyList_GetItem(items, 0);
if (item == NULL) return NULL;
Py_INCREF(item);

这几行没有写锁,却可能依赖 CPython 的全局解释器锁 GIL。撤掉它,原本正确的代码就可能访问已经释放的内存。

社区已经在解决 GIL。CPython 3.13 提供了可选的自由线程构建,可以在关闭 GIL 的情况下运行;3.14 将它推进到正式支持、仍然可选的阶段。长期难以直接移除 GIL 的一项重要原因,是旧代码把它提供的保护当成了前提。解释器需要补上新的保证,扩展也需要检查自己的使用方式。开头这三行就是一个具体例子。

第一行取得的是对象的地址,没有复制对象,也没有替调用者取得一份持有权。这叫“借用引用”。item 这个 C 变量记住了地址,不会自动阻止对象被回收。

Py_INCREF(item) 才表示:这段代码也要持有这个对象。取得的这份持有权叫“强引用”,不用时要通过 Py_DECREF 释放。

假设列表本身始终有效,里面只有一个普通、可回收的对象 X,而且列表是 X 的唯一持有者。列表还持有 X 时,X 不会被回收;列表放手以后,借来的地址就未必能用了。

在启用 GIL 的情况下,执行这几行 C 代码的线程持有 GIL,中途没有释放它,也没有回调其他 Python 代码。同一解释器内,别的 Python 线程便无法在取值和 Py_INCREF 之间插进来删除列表元素。调用者可以先借到地址,再取得自己的强引用。

关掉 GIL,同时允许另一线程修改这个列表,顺序就可能变成:

线程 A 取得 X 的地址,还没执行 Py_INCREF。

线程 B 执行 del items[0],释放列表对 X 的最后一份强引用。X 可能随之被销毁。

线程 A 继续执行 Py_INCREF(item),尝试访问 X 的引用计数,此时那块内存可能已经失效。

A 手里虽然有地址,却还没来得及让这个地址对应的对象继续活着。

把引用计数的加一改成原子操作,也不能单独修复它。原子加一可以避免多个线程更新同一个计数时相互覆盖,却不能保证加一之前,对象仍然存在。对已经失效的内存做原子操作,依然是错误的访问。

即便给旧取值函数内部加锁,如果函数返回时就解锁,B 仍能在返回之后、Py_INCREF 之前删除 X。调用者需要的保护,必须持续到自己取得强引用。只保证“取值这一步安全”,覆盖不到两次调用之间的空隙。

CPython 从 3.13 起提供了另一个接口:

PyObject *item = PyList_GetItemRef(items, 0);
if (item == NULL) return NULL;
/* 使用 item */
Py_DECREF(item);

PyList_GetItemRef 成功返回时,调用者已经持有一份强引用。取值与取得引用之间需要的同步,由解释器内部处理。之后即使列表删除 X,也只是释放列表持有的那一份,调用者仍然可以使用 X,直到自己释放引用。

这份引用保证对象继续存活。若其他线程还会修改对象的内容,仍然需要相应的同步。

旧接口又不能直接改成这种返回规则。原来只借用、没有安排释放的调用方,会漏掉新增引用的释放;开头已经执行 Py_INCREF 的代码,则会额外多持有一份。新增接口以后,调用方还得调整取得和释放引用的方式。

审查这类代码时,要检查的是:从借到地址,到取得强引用,这段空隙里,另一个线程是否可能移除并释放对象。 如果列表不会被并发修改,或者已有同步保护覆盖整个过程,这条风险就不成立。同一个旧接口,可能在一处需要迁移,在另一处仍然安全。

这也是自由线程支持需要扩展维护者参与的具体原因。在 CPython 3.14 的自由线程构建中,未强制关闭 GIL 时,导入一个没有声明支持自由线程的 C 扩展,会触发警告并重新启用 GIL。维护者作出支持声明之前,需要检查包括开头这三行在内的旧依赖;声明本身不会替代码补上缺失的保护。

以上就是“为什么 Python 的 GIL 问题一直让人诟病,Python 社区却不解决?”的详细内容,想要了解更多Python教程欢迎持续关注编程学习网。 

扫码二维码 获取免费视频学习资料

Python编程学习

查 看2022高级编程视频教程免费获取