【问题标题】:Python C API - Is it thread safe?Python C API - 它是线程安全的吗?
【发布时间】:2017-06-19 18:25:27
【问题描述】:

我有一个从我的多线程 Python 应用程序调用的 C 扩展。我在 C 函数的某处使用了静态变量 i,稍后我有一些 i++ 语句可以从不同的 Python 线程运行(该变量仅在我的 C 代码中使用,但我不屈服到 Python)。

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

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

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

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

【问题讨论】:

  • “我知道 GIL 只保证单个字节码指令是原子的和线程安全的”——它甚至不保证这一点。不过,您在 C 中的 i++ 应该没问题;中间不能释放 GIL。 C 代码不会释放 GIL,除非它进行显式调用以让其他线程有机会运行(但要小心调用您无法控制的代码,这可能会为您执行该调用)。
  • 哇,我现在更困惑了。我读到here 说单字节码指令是线程安全的......你是什么意思,除非明确告知,C 代码永远不会释放 GIL?就像,即使我放了sleep 或一些等待/IO 指令?一旦你输入 C 代码,它只是一个单一的原子执行?
  • 常见的误解,但不,它们不是线程安全的,最明显的是因为单个 BINARY_ADD 或任何操作码可以解析为用 Python 编写的任意用户定义函数。您必须确保执行操作码不会导致调用其他 Python 代码,并且任何涉及的 C 代码都不会显式释放 GIL。

标签: c multithreading python-2.7 python-c-api cpython


【解决方案1】:

当您运行 C 代码时,Python 不会释放 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 中的线程安全。最简单的方法是仅使用局部变量而不读取或写入任何全局状态。

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


其他想法

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


迟来的编辑:

需要强调的是,导致 Python 代码的执行真的非常非常容易。出于这个原因,您不应该使用 GIL 来代替互斥锁或实际的锁定机制。您应该只将它用于真正原子的操作(即单个 C API 调用)或完全在非 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

【讨论】:

  • 这很大,我不知道从解释器的角度来看,对 C 扩展函数的调用是原子的。这意味着如果您打算花一些时间在您的 C 扩展中,您必须明确告诉您的代码释放 GIL,否则您甚至无法让其他线程进行等待 IO 完成的调用?无论如何,感谢您的洞察力。有没有关于那个的文件?我什么也没找到。
  • 逻辑是你需要持有 GIL 才能使用任何 Python API 调用(如果你不这样做,它通常会出现段错误)。如果只是为了发布 GIL 本身,那么您永远无法确定任何事情都是安全的。因此,它依赖于您自己判断何时不需要 GIL。虽然您拥有 GIL,但不会启动任何新线程,但如果其他线程已经等待 IO 完成,那么它们将在您的 C 函数运行时继续在后台执行此操作(但它们不会做任何 Pythony直到你放弃 GIL)
  • 我正在努力寻找明确表示我害怕的好的文档。如果我找到一些我会链接它。不过它很容易测试:设置一组运行的线程,定期打印“Hello from thread A/B/C ...”,然后创建另一个线程调用一个 C 函数并休眠一分钟。
  • 这是第一段的参考:docs.python.org/3/faq/…“因此,从 Python 程序的角度来看,每条字节码指令以及从每条指令到达的所有 C 实现代码都是原子的。”跨度>
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-01-22
  • 2011-11-09
  • 1970-01-01
相关资源
最近更新 更多