PEP 311 – 简化扩展的全局解释器锁获取
- 作者:
- Mark Hammond <mhammond at skippinet.com.au>
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建日期:
- 2003年2月5日
- Python 版本:
- 2.3
- 发布历史:
- 2003年2月5日,2003年2月14日,2003年4月19日
摘要
本 PEP 提出了一种简化的 API,用于 Python 扩展模块访问全局解释器锁 (GIL)。具体来说,它为复杂的多线程扩展的作者提供了一个解决方案,在这些扩展中,Python 的当前状态(即 GIL 的状态)是未知的。
本 PEP 提出了一种新的 API,用于支持线程的平台,以管理 Python 线程状态。本文提出了一种实现策略,以及一个初步的、平台独立的实现。
基本原理
当前的 Python 解释器状态 API 适用于简单的单线程扩展,但对于非简单的多线程扩展来说,它很快就会变得异常复杂。
目前 Python 提供了两种处理 GIL 的机制
Py_BEGIN_ALLOW_THREADS
和Py_END_ALLOW_THREADS
宏。这些宏主要用于允许一个已经拥有 GIL 的简单 Python 扩展在进行“外部”(即非 Python)且通常代价高昂的调用时暂时释放它。任何正在等待 GIL 的现有 Python 线程都可以自由运行。虽然这对于从 Python 调用到外部世界的扩展来说是很好的,但对于在线程状态未知时需要调用 Python 的扩展来说,这没有任何帮助。PyThreadState
和PyInterpreterState
API。这些 API 函数允许扩展/嵌入式应用程序获取 GIL,但它们面临一个严重的引导问题——在使用它们之前,您需要知道 Python 解释器和 GIL 的状态。一个特别的问题是对于需要处理 Python 从未见过的线程,但需要从该线程调用 Python 的扩展作者来说。编写一个这些“新”线程总是知道 GIL 的确切状态,从而可以可靠地与此 API 交互的扩展是非常困难、微妙且容易出错的。
由于这些原因,此类扩展应如何与 Python 交互的问题正迅速成为一个常见问题。本 PEP 的主要推动力是 python-dev 上的一个帖子[1],它立即指出了具有此确切问题的以下项目
- win32all 扩展
- Boost
- ctypes
- Python-GTK 绑定
- Uno
- PyObjC
- Mac 工具箱
- PyXPCOM
目前,对于这个问题没有合理、可移植的解决方案,迫使每个扩展作者都实现自己的定制版本。此外,问题很复杂,这意味着许多实现很可能不正确,导致各种问题,通常只会表现为“Python 已挂起”。
虽然现有线程状态 API 中最大的问题是无法查询锁的当前状态,但我们认为应该为扩展作者提供一个更完整、更简化的解决方案。这样的解决方案应该鼓励作者提供无错误、复杂的扩展模块,充分利用 Python 的线程机制。
限制和排除
本提案确定了针对具有复杂多线程需求但只需要一个“PyInterpreterState”的扩展作者的解决方案。不尝试迎合需要多个解释器状态的扩展。在撰写本文时,尚未发现需要多个 PyInterpreterStates 的扩展,而且尚不清楚该功能在 Python 本身中是否正常工作。
此 API 不会执行 Python 的自动初始化,也不会为多线程操作初始化 Python。扩展作者必须继续调用 Py_Initialize()
,对于多线程应用程序,必须调用 PyEval_InitThreads()
。这样做的原因是,第一个调用 PyEval_InitThreads()
的线程被 Python 指定为“主线程”,因此强制扩展作者指定主线程(通过要求他们进行第一次调用)消除了歧义。由于 Py_Initialize()
必须在 PyEval_InitThreads()
之前调用,并且由于这两个函数目前都支持多次调用,因此这给扩展作者带来的负担被认为是合理的。
此 API 旨在成为获取 Python GIL 所必需的全部。除了现有标准 Py_BEGIN_ALLOW_THREADS
和 Py_END_ALLOW_THREADS
宏之外,假设扩展不会使用任何额外的线程状态 API 函数。具有此类复杂需求的扩展可以自由地继续使用现有线程状态 API。
提案
本提案建议向 Python 添加一个新的 API,以简化 GIL 的管理。此 API 将在所有定义了 WITH_THREAD
的平台上可用。
其目的是,假设 Python 已正确初始化,扩展作者可以在任何时候、在任何线程上使用一个小的、定义明确的“序言舞”,这将确保 Python 在该线程上准备好使用。在扩展完成 Python 后,它还必须执行“尾声舞”以释放以前获取的任何资源。理想情况下,这些舞步可以用一行代码表达。
具体来说,建议使用以下新 API
/* Ensure that the current thread is ready to call the Python
C API, regardless of the current state of Python, or of its
thread lock. This may be called as many times as desired
by a thread so long as each call is matched with a call to
PyGILState_Release(). In general, other thread-state APIs may
be used between _Ensure() and _Release() calls, so long as the
thread-state is restored to its previous state before the Release().
For example, normal use of the Py_BEGIN_ALLOW_THREADS/
Py_END_ALLOW_THREADS macros are acceptable.
The return value is an opaque "handle" to the thread state when
PyGILState_Acquire() was called, and must be passed to
PyGILState_Release() to ensure Python is left in the same state. Even
though recursive calls are allowed, these handles can *not* be
shared - each unique call to PyGILState_Ensure must save the handle
for its call to PyGILState_Release.
When the function returns, the current thread will hold the GIL.
Failure is a fatal error.
*/
PyAPI_FUNC(PyGILState_STATE) PyGILState_Ensure(void);
/* Release any resources previously acquired. After this call, Python's
state will be the same as it was prior to the corresponding
PyGILState_Acquire call (but generally this state will be unknown to
the caller, hence the use of the GILState API.)
Every call to PyGILState_Ensure must be matched by a call to
PyGILState_Release on the same thread.
*/
PyAPI_FUNC(void) PyGILState_Release(PyGILState_STATE);
常见用法是
void SomeCFunction(void)
{
/* ensure we hold the lock */
PyGILState_STATE state = PyGILState_Ensure();
/* Use the Python API */
...
/* Restore the state of Python */
PyGILState_Release(state);
}
设计与实现
PyGILState_Ensure()
的一般操作将是
- 断言 Python 已初始化。
- 为当前线程获取一个
PyThreadState
,如果需要则创建并保存。 - 记住锁的当前状态(已拥有/未拥有)
- 如果当前状态未拥有 GIL,则获取它。
- 增加当前线程上调用
PyGILState_Ensure
的次数计数器。 - 返回
PyGILState_Release()
的一般操作将是
- 断言我们的线程当前持有锁。
- 如果旧状态表明锁以前是解锁的,则释放 GIL。
- 递减线程的
PyGILState_Ensure
计数器。 - 如果计数器 == 0
- 释放并删除
PyThreadState
。 - 忘记
ThreadState
被线程拥有。
- 释放并删除
- 返回
假设如果单个线程使用两个独立的 PyThreadStates
,则这是一个错误。pystate.h
中的注释(“State unique per thread”)支持此观点,尽管从未直接说明。因此,这将需要一些线程本地存储的实现。幸运的是,Python 源代码树中已经存在线程本地存储的平台独立实现,位于 SGI 线程端口中。此代码将集成到平台独立的 Python 核心中,但以这样一种方式,如果需要,平台可以提供更优化的实现。
实施
此提案的实现可以在 https://bugs.python.org/issue684256 找到
参考资料
版权
本文档已置于公共领域。
来源: https://github.com/python/peps/blob/main/peps/pep-0311.rst
上次修改: 2025-02-01 08:59:27 GMT