小编典典

Python 3中的PyEval_InitThreads:如何/何时调用它?

python

基本上,在什么时候应该准确调用什么以及需要什么伴随的API调用方面似乎存在 巨大的 困惑/模糊性
PyEval_InitThreads()

。不幸的是,Python官方文档非常模糊。关于这个话题,已经有很多关于stackoverflow的问题,因此,如果将其作为副本关闭,我不会感到特别惊讶。但是请考虑一下,这个问题似乎没有确定的答案。(可悲的是,我没有快速拨号上的Guido VanRossum。)

首先,让我们在这里定义问题的范围: 我想做什么? 好吧…我想用C写一个Python扩展模块,它将:

  1. 使用pthreadC语言中的API生成工作线程
  2. 从这些C线程中调用Python回调

好的,让我们从Python文档本身开始。在
Python的3.2文档

说:

无效PyEval_InitThreads()

初始化并获取全局解释器锁。在创建第二个线程或进行任何其他线程操作(例如PyEval_ReleaseThread(tstate))之前,应在主线程中调用它。在调用PyEval_SaveThread()或PyEval_RestoreThread()之前不需要它。

所以我的理解是:

  1. 产生线程的任何C扩展模块都必须PyEval_InitThreads()在产生任何其他线程之前从主线程调用
  2. 调用会PyEval_InitThreads锁定GIL

因此,常识告诉我们,任何创建线程的C扩展模块都必须调用PyEval_InitThreads(),然后释放全局解释器锁。好吧,看起来很简单。因此,
表面看来 ,所需的只是以下代码:

PyEval_InitThreads(); /* initialize threading and acquire GIL */
PyEval_ReleaseLock(); /* Release GIL */

似乎很容易…但是不幸的是,Python 3.2文档 说它 PyEval_ReleaseLock 已被
弃用

。相反,我们应该使用
PyEval_SaveThread

它来发布GIL:

PyThreadState * PyEval_SaveThread()

释放全局解释器锁(如果已创建并启用了线程支持),然后将线程状态重置为NULL,返回先前的线程状态(非NULL)。如果已创建锁,则当前线程必须已获取它。

嗯…好吧,所以我猜一个C扩展模块需要说:

PyEval_InitThreads();
PyThreadState* st = PyEval_SaveThread();

确实,这正是
这个stackoverflow答案
所说的。除非我在实践中实际 尝试
过,否则在导入扩展模块时,Python解释器会立即出现段错误。真好


好的,现在我放弃正式的Python文档,转向Google。因此,
这个随机博客
声称您需要从扩展模块执行的所有操作是PyEval_InitThreads()。当然,文档声称PyEval_InitThreads()获得了GIL,实际上,

快速检查PyEval_InitThreads()in的源代码ceval.c
表明它确实调用了内部功能。take_gil(PyThreadState_GET());

因此PyEval_InitThreads() 一定要 获得GIL。我认为那您绝对需要在致电后以某种方式释放GIL
PyEval_InitThreads()但是如何?
PyEval_ReleaseLock()已过时,并且出现PyEval_SaveThread()了莫名其妙的段错误。

好了…所以也许出于某种原因这是目前我无法理解,一个C扩展模块 并不
需要释放GIL。我尝试了一下……并且,正如预期的那样,一旦另一个线程尝试获取GIL(使用
PyGILState_Ensure

),程序就会从死锁中挂起。是的…您 真的 需要在致电后释放GIL PyEval_InitThreads()

同样,问题是:
在调用后如何释放GILPyEval_InitThreads()

更笼统地说: 为了能够从辅助C线程安全地调用Python代码,C扩展模块到底需要做什么?


阅读 448

收藏
2020-12-20

共1个答案

小编典典

您的理解是正确的:调用PyEval_InitThreads确实可以获取GIL。在正确编写的Python /
C应用程序中,这不是问题,因为GIL将自动或手动及时解锁。

如果主线程继续运行Python代码,则没有什么特别的事情,因为Python解释器将在执行了许多指令后自动放弃GIL(允许另一个线程来获取它,它将再次放弃它,依此类推)上)。另外,每当Python要调用阻塞系统调用时(例如,从网络读取或写入文件),它将在调用周围释放GIL。

这个答案的原始版本到此为止。但是还有另外一件事要考虑: 嵌入 方案。

嵌入Python时,主线程通常会初始化Python并继续执行其他与Python不相关的任务。在那种情况下,没有什么东西会 自动
释放GIL,因此必须由线程本身完成。这绝不是特定于call的调用PyEval_InitThreads,它是在使用获取的GIL调用的所有Python /
C代码中
所期望的。

例如,main()可能包含以下代码:

Py_Initialize();
PyEval_InitThreads();

Py_BEGIN_ALLOW_THREADS
... call the non-Python part of the application here ...
Py_END_ALLOW_THREADS

Py_Finalize();

如果您的代码是手动创建线程的,则它们需要先获取GIL,然后再进行与Python相关的 任何操作
,甚至简单到Py_INCREF。为此,请使用以下命令

// Acquire the GIL
PyGILState_STATE gstate;
gstate = PyGILState_Ensure();

... call Python code here ...

// Release the GIL. No Python API allowed beyond this point.
PyGILState_Release(gstate);
2020-12-20