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指定为主线程,因此,强制扩展作者指定主线程(通过要求他们进行此首次调用)消除了歧义。由于必须在PyEval_InitThreads()
之前调用Py_Initialize()
,并且这两个函数目前都支持多次调用,因此这对扩展作者造成的负担被认为是合理的。
预期此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”)支持这种观点,尽管从未直接说明。因此,这将需要一些线程本地存储的实现。幸运的是,Python源代码树中(在SGI线程端口中)已经存在线程本地存储的平台无关实现。此代码将集成到平台无关的Python核心,但以一种方式实现,以便平台可以根据需要提供更优化的实现。
实现
此提案的实现可以在 https://bugs.python.org/issue684256 找到
参考文献
版权
本文档已进入公有领域。
来源: https://github.com/python/peps/blob/main/peps/pep-0311.rst
上次修改: 2023年9月9日 17:39:29 GMT