Python C API - 它是线程安全的吗?

Python C API - Is it thread safe?

我有一个从我的多线程 Python 应用程序调用的 C 扩展。我在 C 函数的某处使用了一个静态变量 i,稍后我有一些 i++ 语句可以是来自不同 Python 线程的 运行(该变量只是尽管在我的 C 代码中使用过,但我不会将其屈服于 Python).

出于某种原因,到目前为止我还没有遇到任何比赛条件,但我想知道这是否只是运气......

我没有任何与线程相关的 C 代码(没有 Py_BEGIN_ALLOW_THREADS 或任何东西)。

我知道 GIL 只保证单个字节码指令是原子的和线程安全的,因此 Python 中的 i+=1 语句不是线程安全的。

但我不知道 C 扩展中的 i++ 指令。有帮助吗?

Python 不会在您使用 运行 C 代码时释放 GIL(除非您告诉它或导致执行 Python 代码 - 请参阅底部的警告说明!)。它仅在字节码指令之前(而不是期间)释放 GIL,从解释器的角度来看 运行 C 函数是执行 CALL_FUNCTION 字节码的一部分。* (不幸的是,我目前找不到这一段的参考资料,但我几乎可以肯定它是正确的)

因此,除非您执行任何特定操作,否则您的 C 代码将是唯一的线程 运行,因此您在其中执行的任何操作都应该是线程安全的。

如果您特别想释放 GIL - 例如,因为您正在进行不干扰 Python 的长时间计算、从文件读取或在等待其他事情发生时休眠- 那么最简单的方法就是 Py_BEGIN_ALLOW_THREADS then Py_END_ALLOW_THREADS when you want to get it back。在此块期间,您不能使用大多数 Python API 函数,您有责任确保 C 中的线程安全。最简单的方法是仅使用局部变量,而不读取或写入任何全局状态。

如果您已经有一个没有 GIL(线程 A)的 C 线程 运行,那么简单地在线程 B 中保留 GIL 并不能保证线程 A 不会修改 C 全局变量。为了安全起见,您需要确保在所有 C 函数中都不会在没有某种锁定机制(Python GIL 或 C 机制)的情况下修改全局状态。


补充思考

* 可以在 C 代码中释放 GIL 的一个地方是 C 代码调用导致 Python 代码执行的内容。这可能是通过使用 PyObject_Call。一个不太明显的地方是如果 Py_DECREF 导致析构函数被执行。当您的 C 代码恢复时,您将获得 GIL,但您无法再保证全局对象未更改。这显然不会影响像 x++.

这样的简单 C

迟来的编辑:

需要强调的是,Python代码的执行真的非常非常容易。出于这个原因,您不应该使用 GIL 代替互斥体或实际的锁定机制。你应该只考虑它用于真正原子的操作(即单个 C API 调用)或完全在 non-Python C 对象上。执行 C 代码时不会意外丢失 GIL,但是很多 C API 调用可能会释放 GIL,执行其他操作,然后在返回 C 代码之前重新获得 GIL。

GIL 的目的是确保 Python 内部结构不被破坏。 GIL 将在一个扩展模块中继续为这个目的服务。然而,涉及以您不希望的方式排列的有效 Python 对象的竞争条件仍然可供您使用。例如:

PySequence_SetItem(some_list, 0, some_item);
PyObject* item = PySequence_GetItem(some_list, 0);
assert(item == some_item); // may not be true 
// the destructor of the previous contents of item 0 may have released the GIL