PEP 788 – 保护 C API 免受解释器终止的影响
- 作者:
- Peter Bierma <zintensitydev at gmail.com>
- 发起人:
- Victor Stinner <vstinner at python.org>
- 讨论至:
- Discourse 帖子
- 状态:
- 草案
- 类型:
- 标准跟踪
- 创建日期:
- 2025年4月23日
- Python 版本:
- 3.15
- 发布历史:
- 2025年3月10日, 2025年4月27日, 2025年5月28日, 2025年10月3日
摘要
本 PEP 引入了一套 C API 函数,用于通过防止解释器终止来安全地附加到解释器。例如:
static int
thread_function(PyInterpreterView view)
{
// Prevent the interpreter from finalizing
PyInterpreterGuard guard = PyInterpreterGuard_FromView(view);
if (guard == 0) {
return -1;
}
// Analogous to PyGILState_Ensure(), but this is thread-safe.
PyThreadView thread_view = PyThreadState_Ensure(guard);
if (thread_view == 0) {
PyInterpreterGuard_Close(guard);
return -1;
}
// Now we can call Python code, without worrying about the thread
// hanging due to finalization.
if (PyRun_SimpleString("print('My hovercraft is full of eels') < 0) {
PyErr_Print();
}
// Destroy the thread state and allow the interpreter to finalize.
PyThreadState_Release(thread_view);
PyInterpreterGuard_Close(guard);
return 0;
}
此外,本提案弃用了 PyGILState
系列中的 API。
背景
在 C API 中,线程可以通过为当前线程持有附加线程状态来与解释器交互。当以安全的方式创建和附加线程状态时,这可能会变得复杂,因为任何非 Python 线程(未通过threading
模块创建的线程)都被认为是“守护”线程,这意味着解释器在关闭之前不会等待该线程。相反,解释器将在线程尝试附加线程状态时挂起该线程,此后使该线程无法使用。
在调用 Python 的任何时候都可能发生附加线程状态,例如在字节码指令之间(将GIL让给不同的线程),或者当 C 函数退出Py_BEGIN_ALLOW_THREADS
块时,因此仅仅防范解释器是否正在终止不足以安全地调用 Python 代码。(请注意,挂起线程是一种相对较新的行为;在旧版本中,线程会退出,但问题是相同的。)
目前,C API 没有提供任何方法来确保解释器处于不会导致线程在尝试附加时挂起的状态。这在需要与其他原生代码一起执行 Python 代码的大型应用程序中可能是一个令人沮丧的问题。
此外,用户创建非 Python 线程的典型模式是使用PyGILState_Ensure()
,该函数在PEP 311中引入。这对子解释器来说非常不幸,因为PyGILState_Ensure()
倾向于为主解释器而不是当前解释器创建线程状态。这导致当扩展创建与 Python 解释器交互的线程时出现线程安全问题,因为对 GIL 的假设是错误的。
动机
非 Python 线程总是在终止期间挂起
许多大型库可能需要在高度异步的情况下调用 Python 代码,而所需的解释器可能正在终止或已被删除,但希望在调用解释器后继续运行代码。这种愿望已被用户提出。例如,想要调用 Python 代码的回调可能会在以下情况被调用:
- GPU 上的内核已完成运行。
- 收到网络数据包。
- 线程已退出,原生库正在为线程局部存储执行静态终止程序。
通常,这种模式看起来像这样:
static void
some_callback(void *closure)
{
/* Do some work */
/* ... */
PyGILState_STATE gstate = PyGILState_Ensure();
/* Invoke the C API to do some computation */
PyGILState_Release(gstate);
/* ... */
}
这意味着任何非 Python 线程都可能在任何时候被终止,这严重限制了希望在调用流中执行不仅仅是 Python 代码的用户。
Py_IsFinalizing
不是原子的
由于前面提到的问题,文档目前建议使用Py_IsFinalizing()
来防止线程终止:
当运行时正在终止时从线程调用此函数将终止线程,即使该线程不是由 Python 创建的。您可以使用Py_IsFinalizing()
或sys.is_finalizing()
来检查解释器是否正在终止,以避免不必要的终止。
不幸的是,这并不可靠,因为存在“调用时”到“使用时”的问题;解释器在调用Py_IsFinalizing()
期间可能没有终止,但可能立即开始终止,这将导致线程状态的附加挂起线程。
用户过去曾表示希望以原子方式调用Py_IsFinalizing
。
原生扩展中的锁在终止期间可能无法使用
在原生 API 中获取锁时,通常会释放 GIL(或自由线程构建上的关键部分)以避免锁顺序死锁。这在终止期间可能会出现问题,因为持有锁的线程可能会被挂起。例如:
- 线程去获取锁,首先分离其线程状态以避免死锁。
- 主线程开始终止并告诉所有线程状态在附加时挂起。
- 线程获取了它正在等待的锁,但随后在尝试通过
Py_END_ALLOW_THREADS
重新附加其线程状态时挂起。 - 主线程无法再获取锁,因为它被持有的线程已挂起。
这会影响 CPython 本身,而且使用当前的 API 无法做太多来修复它。例如,python/cpython#129536指出ssl
模块在终止时使用时可能会发出致命错误,因为一个守护线程在持有sys.stderr
的锁时被挂起,然后一个终止程序试图写入它。理想情况下,线程应该能够在持有锁时暂时阻止解释器挂起它。
PyGILState_Ensure
的终止行为无法更改
在 Python 程序中,总会有一个点,PyGILState_Ensure()
无法再附加线程状态。如果解释器早已死亡,那么 Python 显然无法给线程一个调用它的方法。PyGILState_Ensure()
没有任何有意义的方式返回失败,因此它别无选择,只能终止线程或发出致命错误,如python/cpython#124622所述:
我认为需要一个新的 GIL 获取和释放 C API。现有的 C 代码中使用现有 API 的方式不适合突然附加错误状态;现有的 C 代码都不是这样编写的。调用后它们总是假定它们拥有 GIL 并可以继续。该 API 被设计为“它会阻塞并只在拥有 GIL 后返回”,没有其他选项。
因此,CPython 无法真正改变PyGILState_Ensure()
在终止期间的工作方式,因为它会破坏现有代码。
“GIL”这个术语对于自由线程来说很棘手
C API 中“GIL”这个术语的一个重要问题是它在语义上具有误导性。本 PEP 的作者在python/cpython#127989中提到了这一点:
最大的问题是,对于自由线程,没有 GIL,因此用户错误地在Py_BEGIN_ALLOW_THREADS
块内调用 C API 或在新线程中省略PyGILState_Ensure
。
再次强调,PyGILState_Ensure()
在带有 GIL 和自由线程构建上都为线程获取一个附加线程状态。调用 C API 总是需要一个附加线程状态,因此在自由线程构建上仍然需要调用PyGILState_Ensure()
,但是以“确保 GIL”这样的名称,并不立即清楚这是真的。
PyGILState_Ensure
无法猜测正确的解释器
正如文档中所述,PyGILState
函数在子解释器中未正式支持:
请注意,PyGILState_*
函数假定只有一个全局解释器(由Py_Initialize()
自动创建)。Python 支持创建额外的解释器(使用Py_NewInterpreter()
),但混合使用多个解释器和PyGILState_*
API 是不受支持的。
这是因为PyGILState_Ensure()
无法知道是哪个解释器创建了线程,因此它必须假定是主解释器。在运行时无法检测到这一点,因此子解释器创建的线程中必然会出现虚假的竞争,因为对错误解释器的同步将用于线程之间共享的对象。
例如,如果线程可以访问属于子解释器的对象 A,但随后调用了PyGILState_Ensure()
,则线程将具有指向主解释器而不是子解释器的附加线程状态。这意味着对对象的所有 GIL 假设都是错误的,因为两个 GIL 之间没有同步。
除了引入一个明确从调用者获取解释器的新 API 之外,没有很好的方法来解决这个问题。
子解释器可以并发地解除分配
创建非 Python 线程的另一种方法,PyThreadState_New()
和PyThreadState_Swap()
,对于支持子解释器要好得多(因为PyThreadState_New()
采用显式解释器,而不是假定请求的是主解释器),但仍然受到 C API 中当前挂起问题的限制,并且当子解释器在线程有机会启动之前终止时,容易导致崩溃。这是因为在子解释器中,PyInterpreterState *
结构是在堆上分配的,而主解释器是静态分配在 Python 运行时状态上的。
基本原理
防止解释器关闭
本 PEP 采用的方法是解释器包含一个保护 API,可防止其关闭。持有解释器保护可确保安全地调用 C API,而无需担心线程因终止而挂起。
这意味着与 Python 交互(例如,在 C++ 库中)将需要一个对解释器的保护才能安全地调用对象,这比假设主解释器是正确的选择更不方便,但确实没有其他选择。
本提案还提供了对解释器的“视图”,可用于安全地探查可能已死亡或存活的解释器。使用视图,用户可以在其生命周期中的任何时候创建解释器保护,如果解释器无法再支持调用 Python 代码,它将安全地失败。
PyGILState_Ensure
的兼容性垫片
本提案附带PyUnstable_InterpreterView_FromDefault()
,作为PyGILState_Ensure()
某些用户的兼容性黑客。这是一种线程安全的方式来为主(或“默认”)解释器创建保护。
将新代码移植到PyThreadState_Ensure()
的主要缺点是它不是PyGILState_Ensure()
的直接替代品,因为它需要一个解释器保护参数。在某些大型应用程序中,将所有地方重构为使用PyInterpreterGuard
可能很棘手,因此此函数是那些明确希望禁止支持子解释器的用户的最后手段。
规范
解释器守卫
-
type PyInterpreterGuard
- 不透明的解释器守卫。
通过持有解释器守卫,调用者可以确保解释器在守卫销毁之前不会终止。
这类似于“读写”锁;线程可以并发持有解释器的守卫,并且解释器必须等到所有线程都销毁了它们的守卫才能进入终止状态。
此类型保证是指针大小。
-
PyInterpreterGuard PyInterpreterGuard_FromCurrent(void)
- 为当前解释器创建终止守卫。
成功时,此函数守卫解释器并返回守卫的不透明引用;失败时,它返回
0
并设置异常。调用者必须持有附加线程状态。
-
PyInterpreterGuard PyInterpreterGuard_FromView(PyInterpreterView view)
- 通过视图为解释器创建终止守卫。
成功时,此函数返回由view表示的解释器的守卫。调用此函数后,视图仍然有效。
如果解释器不再存在或无法安全运行 Python 代码,此函数返回
0
而不设置异常。调用者不需要持有附加线程状态。
-
PyInterpreterState *PyInterpreterGuard_GetInterpreter(PyInterpreterGuard guard)
- 返回由guard保护的
PyInterpreterState
指针。此函数不会失败,并且调用者不需要持有附加线程状态。
-
PyInterpreterGuard PyInterpreterGuard_Copy(PyInterpreterGuard guard)
- 复制一个解释器守卫。
成功时,此函数返回guard的副本;失败时,它返回
0
而不设置异常。调用者不需要持有附加线程状态。
-
void PyInterpreterGuard_Close(PyInterpreterGuard guard)
- 销毁一个解释器守卫,如果不再有其他守卫,则允许解释器进入终止状态。
此函数不会失败,并且调用者不需要持有附加线程状态。
解释器视图
-
type PyInterpreterView
- 解释器的不透明视图。
这是一种线程安全的方式来访问可能在另一个线程中终止的解释器。
此类型保证是指针大小。
-
PyInterpreterView PyInterpreterView_FromCurrent(void)
- 创建当前解释器的视图。
此函数通常与
PyInterpreterGuard_FromView()
一起使用。成功时,此函数返回当前解释器的视图;失败时,它返回
0
并设置异常。调用者必须持有附加线程状态。
-
PyInterpreterView PyInterpreterView_Copy(PyInterpreterView view)
- 复制一个解释器视图。
成功时,此函数返回view的非零副本;失败时,它返回
0
而不设置异常。此函数不会失败,并且调用者不需要持有附加线程状态。
-
void PyInterpreterView_Close(PyInterpreterView view)
- 删除解释器视图。
此函数不会失败,并且调用者不需要持有附加线程状态。
-
PyInterpreterView PyUnstable_InterpreterView_FromDefault()
- 为任意“主”解释器创建一个视图。
此函数仅用于无法保存特定解释器的特殊情况。
成功时,此函数返回主解释器的视图;失败时,它返回
0
而不设置异常。调用者不需要持有附加线程状态。
确保和释放线程状态
本提案包括两个新的高级线程 API,旨在替换PyGILState_Ensure()
和PyGILState_Release()
。
-
type PyThreadView
- 一个线程状态的不透明视图。
在本 PEP 中,线程视图除了PyThreadState*指针之外不提供任何额外属性。但是,将来可能会添加
PyThreadView
的 API。此类型保证是指针大小。
-
PyThreadView PyThreadState_Ensure(PyInterpreterGuard guard)
- 确保线程具有受guard保护的解释器的附加线程状态,从而可以安全地调用该解释器。如果线程已经具有附加线程状态,则可以调用此函数,只要随后调用
PyThreadState_Release()
与此匹配即可。此函数的嵌套调用只会有时创建新的线程状态。如果没有附加线程状态,则此函数将检查此线程使用的最新附加线程状态。如果不存在或与guard不匹配,则会创建新的线程状态。如果匹配guard,则会重新附加。如果有附加线程状态,则会进行类似的检查;如果解释器与guard匹配,则会附加,否则会创建新的线程状态。
成功时返回旧线程状态的非零线程视图,失败时返回
0
。
-
void PyThreadState_Release(PyThreadView view)
- 释放
PyThreadState_Ensure()
调用。在相应的
PyThreadState_Ensure()
调用之前的附加线程状态保证在返回时恢复。由PyThreadState_Ensure()
和PyGILState_Ensure()
使用的缓存线程状态(“GIL 状态”)也将恢复。此函数不会失败。
PyGILState
API 的弃用
本 PEP 弃用了所有现有的PyGILState
API,转而支持现有和新的PyThreadState
API。具体如下:
PyGILState_Ensure()
:改用PyThreadState_Ensure()
。PyGILState_Release()
:改用PyThreadState_Release()
。PyGILState_GetThisThreadState()
:改用PyThreadState_Get()
或PyThreadState_GetUnchecked()
。PyGILState_Check()
:改用PyThreadState_GetUnchecked() != NULL
。
所有PyGILState
API 都将在 Python 3.20 中从非受限 C API 中移除。它们将保留在稳定 ABI 中以实现兼容性。
向后兼容性
本 PEP 指定了一个重大更改,即在 Python 3.20 中从非受限 C API 的公共头文件中移除所有PyGILState
API。
安全隐患
本 PEP 没有已知的安全隐患。
如何教授此内容
与所有 C API 函数一样,本 PEP 中的所有新 API 都将在 C API 文档中记录,最好在“非 Python 创建的线程”部分。现有的PyGILState
文档应相应更新以指向新的 API。
示例
这些示例旨在帮助理解本 PEP 中描述的 API。它们可以在文档中重复使用。
示例:库接口
假设您正在开发一个用于日志记录的 C 库。您可能希望提供一个 API,允许用户记录到 Python 文件对象。
根据本 PEP,您将这样实现它:
int
LogToPyFile(PyInterpreterView view,
PyObject *file,
PyObject *text)
{
PyInterpreterGuard guard = PyInterpreterGuard_FromView(view);
if (guard == 0) {
/* Python interpreter has shut down */
return -1;
}
PyThreadView thread_view = PyThreadState_Ensure(guard);
if (thread_view == 0) {
PyInterpreterGuard_Close(guard);
fputs("Cannot call Python.\n", stderr);
return -1;
}
const char *to_write = PyUnicode_AsUTF8(text);
if (to_write == NULL) {
// Since the exception may be destroyed upon calling PyThreadState_Release(),
// print out the exception ourselves.
PyErr_Print();
PyThreadState_Release(thread_view);
PyInterpreterGuard_Close(guard);
return -1;
}
int res = PyFile_WriteString(to_write, file);
free(to_write);
if (res < 0) {
PyErr_Print();
}
PyThreadState_Release(thread_view);
PyInterpreterGuard_Close(guard);
return res < 0;
}
示例:单线程确保
此示例展示了如何在从 C 定义的 Python 方法中获取 C 锁。
如果从守护线程调用此函数,解释器可能会在重新附加其线程状态时挂起该线程,导致我们持有锁。任何未来尝试获取锁的终结器都将死锁。
static PyObject *
my_critical_operation(PyObject *self, PyObject *Py_UNUSED(args))
{
assert(PyThreadState_GetUnchecked() != NULL);
PyInterpreterGuard guard = PyInterpreterGuard_FromCurrent();
if (guard == 0) {
/* Python interpreter has shut down */
return NULL;
}
Py_BEGIN_ALLOW_THREADS;
acquire_some_lock();
/* Do something while holding the lock.
The interpreter won't finalize during this period. */
// ...
release_some_lock();
Py_END_ALLOW_THREADS;
PyInterpreterGuard_Close(guard);
Py_RETURN_NONE;
}
示例:从旧函数过渡
以下代码使用PyGILState
API:
static int
thread_func(void *arg)
{
PyGILState_STATE gstate = PyGILState_Ensure();
/* It's not an issue in this example, but we just attached
a thread state for the main interpreter. If my_method() was
originally called in a subinterpreter, then we would be unable
to safely interact with any objects from it. */
if (PyRun_SimpleString("print(42)") < 0) {
PyErr_Print();
}
PyGILState_Release(gstate);
return 0;
}
static PyObject *
my_method(PyObject *self, PyObject *unused)
{
PyThread_handle_t handle;
PyThead_indent_t indent;
if (PyThread_start_joinable_thread(thread_func, NULL, &ident, &handle) < 0) {
return NULL;
}
Py_BEGIN_ALLOW_THREADS;
PyThread_join_thread(handle);
Py_END_ALLOW_THREADS;
Py_RETURN_NONE;
}
这是相同的代码,重写后使用新函数:
static int
thread_func(void *arg)
{
PyInterpreterGuard guard = (PyInterpreterGuard)arg;
PyThreadView thread_view = PyThreadState_Ensure(guard);
if (thread_view == 0) {
PyInterpreterGuard_Close(guard);
return -1;
}
if (PyRun_SimpleString("print(42)") < 0) {
PyErr_Print();
}
PyThreadState_Release(thread_view);
PyInterpreterGuard_Close(guard);
return 0;
}
static PyObject *
my_method(PyObject *self, PyObject *unused)
{
PyThread_handle_t handle;
PyThead_indent_t indent;
PyInterpreterGuard guard = PyInterpreterGuard_FromCurrent();
if (guard == 0) {
return NULL;
}
if (PyThread_start_joinable_thread(thread_func, (void *)guard, &ident, &handle) < 0) {
PyInterpreterGuard_Close(guard);
return NULL;
}
Py_BEGIN_ALLOW_THREADS
PyThread_join_thread(handle);
Py_END_ALLOW_THREADS
Py_RETURN_NONE;
}
示例:守护线程
有了本 PEP,守护线程与今天的 C API 中非 Python 线程的工作方式非常相似。调用PyThreadState_Ensure()
后,只需关闭解释器守卫,即可让解释器关闭(并永远挂起当前线程)。
static int
thread_func(void *arg)
{
PyInterpreterGuard guard = (PyInterpreterGuard)arg;
PyThreadView thread_view = PyThreadState_Ensure(guard);
if (thread_view == 0) {
PyInterpreterGuard_Close(guard);
return -1;
}
/* Close the interpreter guard, allowing it to
finalize. This means that print(42) can hang this thread. */
PyInterpreterGuard_Close(guard);
if (PyRun_SimpleString("print(42)") < 0) {
PyErr_Print();
}
PyThreadState_Release(thread_view);
return 0;
}
static PyObject *
my_method(PyObject *self, PyObject *unused)
{
PyThread_handle_t handle;
PyThead_indent_t indent;
PyInterpreterGuard guard = PyInterpreterGuard_FromCurrent();
if (guard == 0) {
return NULL;
}
if (PyThread_start_joinable_thread(thread_func, (void *)guard, &ident, &handle) < 0) {
PyInterpreterGuard_Close(guard);
return NULL;
}
Py_RETURN_NONE;
}
示例:异步回调
typedef struct {
PyInterpreterView view;
} ThreadData;
static int
async_callback(void *arg)
{
ThreadData *tdata = (ThreadData *)arg;
PyInterpreterView view = tdata->view;
PyInterpreterGuard guard = PyInterpreterGuard_FromView(view);
if (guard == 0) {
fputs("Python has shut down!\n", stderr);
return -1;
}
PyThreadView thread_view = PyThreadState_Ensure(guard);
if (thread_view == 0) {
PyInterpreterGuard_Close(guard);
return -1;
}
if (PyRun_SimpleString("print(42)") < 0) {
PyErr_Print();
}
PyThreadState_Release(thread_view);
PyInterpreterGuard_Close(guard);
PyInterpreterView_Close(view);
PyMem_RawFree(tdata);
return 0;
}
static PyObject *
setup_callback(PyObject *self, PyObject *unused)
{
// View to the interpreter. It won't wait on the callback
// to finalize.
ThreadData *tdata = PyMem_RawMalloc(sizeof(ThreadData));
if (tdata == NULL) {
PyErr_NoMemory();
return NULL;
}
PyInterpreterView view = PyInterpreterView_FromCurrent();
if (view == 0) {
PyMem_RawFree(tdata);
return NULL;
}
tdata->view = view;
register_callback(async_callback, tdata);
Py_RETURN_NONE;
}
示例:调用 Python 而没有回调参数
在某些情况下,回调函数不带回调参数(void *arg
),因此很难为任何特定解释器创建守卫。解决此问题的方法是通过PyUnstable_InterpreterView_FromDefault()
为主解释器创建守卫。
static void
call_python(void)
{
PyInterpreterView view = PyUnstable_InterpreterView_FromDefault();
if (guard == 0) {
fputs("Python has shut down.", stderr);
return;
}
PyInterpreterGuard guard = PyInterpreterGuard_FromView(view);
if (guard == 0) {
fputs("Python has shut down.", stderr);
return;
}
PyThreadView thread_view = PyThreadState_Ensure(guard);
if (thread_view == 0) {
PyInterpreterGuard_Close(guard);
PyInterpreterView_Close(view);
return -1;
}
if (PyRun_SimpleString("print(42)") < 0) {
PyErr_Print();
}
PyThreadState_Release(thread_view);
PyInterpreterGuard_Close(guard);
PyInterpreterView_Close(view);
return 0;
}
参考实现
本 PEP 的参考实现可在python/cpython#133110找到。
未解决的问题
API 应该如何失败?
关于PyInterpreter[Guard|View]
API 应该如何向调用者指示失败,存在一些分歧。有两种相互竞争的想法:
- 返回 -1 表示失败,返回 0 表示成功。成功时,函数将赋值给作为参数传递的
PyInterpreter[Guard|View]
指针。 - 直接返回
PyInterpreter[Guard|View]
,其中值为 0 等同于NULL
,表示失败。
目前,PEP 拼写了后者。
被拒绝的想法
解释器引用计数
该提案有两个迭代版本,都指定解释器维护一个引用计数,并在该计数达到零之前等待其关闭。
这个想法的第一个迭代通过向PyInterpreterState *
指针添加隐式引用计数来做到这一点。一个名为PyInterpreterState_Hold
的函数会增加引用计数(使其成为“强引用”),而PyInterpreterState_Release
会减少它。解释器的 ID(一个独立的int64_t
)被用作一种弱引用形式,可用于查找解释器状态并原子地增加其引用计数。这些想法最终被拒绝,因为它们似乎使事情变得非常混乱。所有现有的PyInterpreterState *
用途都将被借用,使得开发人员难以理解其代码的哪些部分需要或使用强引用。
为了回应这一反对意见,本 PEP 指定了PyInterpreterRef
API,它们也将模仿引用计数,但以一种更明确的方式,使开发人员更容易理解。PyInterpreterRef
类似于本 PEP 中的PyInterpreterGuard
。同样,较旧的版本包含PyInterpreterWeakRef
,它类似于PyInterpreterView
。
最终,引用计数的概念因以下几个原因完全被本提案放弃:
- API 设计中存在过度复杂化的争议;引用计数设计与 HPy 的非常相似,这在 CPython 中没有先例。有人担心本提案为了更像 HPy 而变得过于复杂。
- 与传统引用计数 API 不同,获取对解释器的强引用可能随时失败,并且当其引用计数达到零时,解释器不会立即解除分配。
- 此前曾讨论过向解释器添加“真正”的引用计数(在达到零时解除分配),如果 CPython 中存在一个名为
PyInterpreterRef
且功能不同的现有 API,那将非常令人困惑。
非守护线程状态
在本 PEP 的早期修订版中,解释器保护是线程状态的属性,而不是解释器的属性。这意味着PyThreadState_Ensure()
保持解释器保护,并在调用PyThreadState_Release()
时关闭它。具有解释器保护的线程状态被称为“非守护线程状态”。起初,这似乎是一种改进,因为它将保护的生命周期管理转移到线程而不是用户,从而消除了一些样板代码。
然而,这最终使提案变得更加复杂,并损害了提案的目标:
- 最重要的是,非守护线程状态过度强调守护线程是问题所在,这使得 PEP 令人困惑。此外,“非守护”这一短语增加了额外的困惑,因为非守护 Python 线程是显式连接的。相比之下,非守护 C 线程只等到它销毁其守卫才被等待。
- 在许多情况下,解释器守卫应该比单个线程状态更长寿。在
PyThreadState_Ensure()
中窃取解释器守卫对于这些情况来说尤其麻烦。如果PyThreadState_Ensure()
没有窃取带有非守护线程状态的守卫,它会混淆解释器守卫的所有权故事,从而导致更令人困惑的 API。
公开 Activate
/Deactivate
API 而不是 Ensure
/Clear
在关于此 API 的先前讨论中,有人建议在 API 中提供实际的PyThreadState
指针,以使线程状态的所有权和生命周期更直观:
更重要的是,我认为这使得线程状态的所有者更清晰——手动创建的线程状态由创建它的代码控制,一旦删除,就不能再激活。
这最终被拒绝,原因有二:
- 提议的 API 与
PyGILState_Ensure()
和PyGILState_Release()
的使用方式更接近,这有助于旧代码库的过渡。 - 对于像 Cython 这样的代码生成器来说,使用起来显著更容易,因为在跟踪
PyThreadState
指针方面没有任何额外的复杂性。
使用 PyStatus
作为 PyThreadState_Ensure
的返回值
在之前对该 API 的迭代中,PyThreadState_Ensure()
返回一个PyStatus
而不是一个整数来表示失败,这具有提供错误消息的好处。
这被拒绝了,因为不清楚错误消息是否那么有用;该 API 的所有设想用例都不会真正关心指示 Python 无法被调用的消息。因此,该 API 只会不必要地变得更复杂,这反过来会损害从PyGILState_Ensure()
的过渡。
此外,PyStatus
在 C API 中不常用。一些与解释器初始化相关的函数使用它(仅仅是因为它们不能引发异常),而PyThreadState_Ensure()
不属于该类别。
致谢
本 PEP 基于许多人的先前工作、反馈和讨论,包括 Victor Stinner、Antoine Pitrou、David Woods、Sam Gross、Matt Page、Ronald Oussoren、Matt Wozniski、Eric Snow、Steve Dower、Petr Viktorin、Gregory P. Smith 和 Alyssa Coghlan。
版权
本文档置于公共领域或 CC0-1.0-Universal 许可证下,以更宽松者为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0788.rst