Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python增强提案

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_THREADSPy_END_ALLOW_THREADS 宏。这些宏主要用于允许一个已经拥有GIL的简单Python扩展在进行“外部”(即非Python)通常代价昂贵的调用时暂时释放它。任何被阻塞等待GIL的现有Python线程现在都可以自由运行。虽然这对于从Python调用外部世界的扩展来说是可以的,但对于需要在线程状态未知时调用Python的扩展却没有帮助。
  • PyThreadStatePyInterpreterState 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_THREADSPy_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