在多线程C应用中嵌入Python

19

我正在将Python解释器嵌入到一个多线程的C应用程序中,对于确保线程安全,我有些困惑,不知道应该使用哪些API。

据我所知,在嵌入Python时,需要在调用任何其他Python C API之前先处理GIL锁,这由嵌入方自行完成。可以使用以下函数来实现:

gstate = PyGILState_Ensure();
// do some python api calls, run python scripts
PyGILState_Release(gstate);

但是这似乎还不够。因为它似乎没有为Python API提供互斥,所以我仍然会遇到随机崩溃。

在阅读了更多的文档后,我还添加了:

PyEval_InitThreads();

在调用Py_IsInitialized()之后立即运行,但这就是令人困惑的部分。文档说明此函数:

初始化并获取全局解释器锁

这表明当函数返回时,GIL应该被锁定,并且应该以某种方式解锁。但实际上似乎不需要这样做。加入这条语句后,我的多线程工作得非常完美,并且通过PyGILState_Ensure/Release函数维护了互斥性。
当我尝试在PyEval_ReleaseLock()之后添加PyEval_ReleaseLock()时,在随后的对PyImport_ExecCodeModule()的调用中很快就会出现死锁。

那么我在这里错过了什么?

4个回答

9

我曾经遇到过完全相同的问题,但是现在我已经解决了。就像你之前建议的那样,在PyEval_InitThreads()之后立即使用PyEval_SaveThread()。不过,我的实际问题是在PyInitialise()之后使用了PyEval_InitThreads(),这导致从其他不同的本地线程调用PyGILState_Ensure()时会被阻塞。总之,现在我这么做:

  1. There is global variable:

    static int gil_init = 0; 
    
  2. From a main thread load the native C extension and start the Python interpreter:

    Py_Initialize() 
    
  3. From multiple other threads my app concurrently makes a lot of calls into the Python/C API:

    if (!gil_init) {
        gil_init = 1;
        PyEval_InitThreads();
        PyEval_SaveThread();
    }
    state = PyGILState_Ensure();
    // Call Python/C API functions...    
    PyGILState_Release(state);
    
  4. From the main thread stop the Python interpreter

    Py_Finalize()
    

我尝试过的所有其他解决方案都会导致随机的Python sigfaults,或者使用PyGILState_Ensure()时会出现死锁/阻塞。

Python文档在此方面应该更加清晰,并至少为嵌入和扩展用例提供示例。


4

最终我弄明白了。
在之后

PyEval_InitThreads();

你需要调用
PyEval_SaveThread();

合理地释放主线程的GIL。


这是错误的且可能会造成危害的:PyEval_SaveThread 应该始终与 PyEval_RestoreThread 结合使用。正如其他地方所解释的,在初始化锁之后不应尝试释放锁;只需让 Python 在其常规工作的一部分中释放它即可。 - user4815162342
我不明白为什么在_Block_ _Allow_块中放置所有对Python的调用会有害。另一方面,如果您不调用PyEval_SaveThread();,则主线程将阻止其他线程访问Python。换句话说,PyGILState_Ensure()会发生死锁。 - khkarens
这是唯一适用于嵌入Python和调用扩展模块的方法。 - Kevin Smyth
1
确实,PyEval_SaveThread() 必须由调用 PyEval_InitThreads() 的线程调用,否则当线程尝试调用 PyGILState_Ensure() 时会发生死锁(因为 GIL 不可用于检索)。PyEval_RestoreThread() 应该最终由调用 PyEval_SaveThread() 的同一线程调用,但此时重要的是所有可能调用 PyGILState_Ensure() 的线程都已完成,否则可能会出现死锁,原因相同。 - andreasdr

1
请注意,@forman的答案中的if (!gil_init) {代码只运行一次,因此可以在主线程中完成,这样可以放弃标志(gil_init必须正确地原子化或以其他方式同步)。 PyEval_InitThreads()仅在CPython 3.6及更早版本中有意义,并且已在CPython 3.9中弃用,因此必须使用宏进行保护。
考虑到所有这些,我目前正在使用以下内容:
在主线程中,运行所有内容。
Py_Initialize();
PyEval_InitThreads(); // only on Python 3.6 or older!
/* tstate = */ PyEval_SaveThread(); // maybe save the return value if you need it later

现在,每当您需要调用Python时,请执行以下操作:
state = PyGILState_Ensure();
// Call Python/C API functions...    
PyGILState_Release(state);

最后,从主线程停止Python解释器

PyGILState_Ensure(); // PyEval_RestoreThread(tstate); seems to work just as well
Py_Finalize()

-2

对于一个多线程的C应用程序,试图从多个线程与单个CPython实例的多个Python线程进行通信,我认为这看起来很危险。

只要有一个C线程与Python通信,即使Python应用程序是多线程的,您也不必担心锁定问题。 如果您需要多个Python线程,可以通过队列将多个C线程与将它们分配给多个Python线程的单个C线程进行通信,以此设置应用程序。

另一种可能适合您的选择是为每个需要它的C线程设置一个CPython实例(当然,Python程序之间的通信应通过C程序进行)。

另一种选择可能是Stackless Python解释器。它摆脱了GIL,但我不确定将其绑定到多个线程是否会遇到其他问题。stackless是我(单线程)C应用程序的替代品。


1
你尽力回答问题,但实际上并没有回答。我不想将工作排队到单个线程中。 - shoosh
你没有回答问题。 - Simon Banks
@SimonBanks ^你的^你是^ - Anthon
@Athhon,我有阅读障碍,它们对我来说都看起来一样。你明白我的意思吧..;-) - Simon Banks

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接