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
我有一个从我的多线程 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++
.
迟来的编辑:
需要强调的是,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