PyGILState_Ensure()导致死锁

PyGILState_Ensure() Causing Deadlock

本文关键字:死锁 Ensure PyGILState      更新时间:2023-10-16

我正在用C++编写一个Python扩展,包装一个我不控制的第三方库。该库创建了一个Python一无所知的线程,并从该线程调用我提供给该库的C++回调。我希望该回调调用Python函数,但使用从文档中读取的方法会出现死锁。以下是我对这些的解释。

void Wrapper::myCallback()
{
PyGILState_STATE gstate=PyGILState_Ensure();
PyObject *result=PyObject_CallMethod(_pyObj,"callback",nullptr);
if (result) Py_DECREF(result);
PyGILState_Release(gstate);
}

我的代码不做任何与线程相关的事情,尽管我已经尝试了许多其他事情。例如,基于此,我尝试调用PyEval_InitThreads(),但不清楚应该在哪里对扩展进行调用。我把它放在PyMODINIT_FUNC里。这些尝试都会导致来自Python的死锁、崩溃或神秘的致命错误,例如PyEval_ReleaseThread:错误的线程状态

这是在带有Python 3.6.1的Linux上。有什么想法可以让这个"简单"的回调工作吗?

可能的过失

我没有意识到在另一个线程中,库处于忙/等待循环中,等待回调的线程。在gdb中,info threads使这一点变得明显。我能看到的唯一解决方案是跳过那些对回调的特定调用;考虑到繁忙/等待的循环,我看不出有什么办法能保证他们的安全。在这种情况下,这是可以接受的,这样做可以消除死锁。

此外,在进行任何操作之前,我似乎也需要调用PyEval_InitThreads()。在C++扩展中,还不清楚它应该去哪里。其中一个回复建议在Python中通过创建和删除一次性threading.Thread来间接执行此操作。这似乎并没有解决问题,反而触发了一个致命的Python错误:take_gil:NULL tstate,我认为这意味着仍然没有gil。基于这一点和它所指的问题,我的猜测是PyEval_InitThreads()导致当前线程成为GIL的主线程。如果这个调用是在短暂的一次性线程中进行的,那么这可能是个问题。是的,我只是猜测,如果有人不必解释,我将不胜感激。

此答案仅适用于Python>=3.0.0。我不知道它是否适用于早期的Python。

将C++模块封装在一个Python模块中,该模块看起来像这样:

import threading
t = threading.Thread(target=lambda: None, daemon=True)
t.run()
del t
from your_cpp_module import *

根据我对文档的阅读,这应该会强制在导入模块之前初始化线程。然后,您在那里编写的回调函数应该可以工作了。

我不太相信这能起作用,但你的模块初始化函数可以这样做:

if (!PyEval_ThreadsInitialized())
{
PyEval_InitThreads();
}

这应该起作用,因为如果PyEval_ThreadsInitialized()不是真的,那么模块init函数应该由唯一存在的Python线程执行,并且持有GIL是正确的做法。

这些都是我的猜测。我从来没有做过这样的事,我对你的问题毫无头绪的评论就是明证。但从我对文档的阅读来看,这两种方法都应该有效。

我是StackOverflow的新手,但在过去几天里,我一直致力于在多线程C++系统中嵌入python,并遇到了许多代码自身死锁的情况。以下是我一直用来确保线程安全的解决方案:

class PyContextManager {
private:
static volatile bool python_threads_initialized;
public:
static std::mutex pyContextLock;
PyContextManager(/* if python_threads_initialized is false, call PyEval_InitThreads and set the variable to true */);
~PyContextManager();
};
#define PY_SAFE_CONTEXT(expr)                   
{                                               
std::unique_lock<std::mutex>(pyContextLock); 
PyGILState_STATE gstate;                     
gstate = PyGILState_Ensure();                
expr;                                     
PyGILState_Release(gstate);                  
}

正在初始化.cpp文件中的布尔值和互斥对象。

我注意到,如果没有互斥,PyGILState_Ensure()命令可能会导致线程死锁。同样,在另一个PySafeContext的expr中调用PySafeContext将导致线程在等待其互斥对象时阻塞。

使用这些函数,我相信你的回调函数会是这样的:

void Wrapper::myCallback()
{
PyContextManager cm();
PY_SAFE_CONTEXT(
PyObject *result=PyObject_CallMethod(_pyObj,"callback",nullptr);
if (result) Py_DECREF(result);
);
}

如果您不认为您的代码可能需要对Python进行多线程调用,那么您可以轻松地扩展宏并从类结构中取出静态变量。这就是我处理未知线程的方式,启动并确定它是否需要启动系统,避免重复编写GIL函数的繁琐。

希望这能有所帮助!

我已经在Python中封装了C++观察器。如果您正在使用boost,那么您可以在boost _PYTHON_MODULE:中调用PyEval_InitThreads()

BOOST_PYTHON_MODULE(eapipy)
{
boost::shared_ptr<Python::InitialisePythonGIL> gil(new Python::InitialisePythonGIL());
....
}

然后,我使用一个类来控制从C++回调到Python。

struct PyLockGIL
{
PyLockGIL()
: gstate(PyGILState_Ensure())
{ 
}
~PyLockGIL()
{
PyGILState_Release(gstate);
}
PyLockGIL(const PyLockGIL&) = delete;
PyLockGIL& operator=(const PyLockGIL&) = delete;
PyGILState_STATE gstate;
};

如果你在任何时间内调用C++,你也可以放弃GIL:

struct PyRelinquishGIL
{
PyRelinquishGIL()
: _thread_state(PyEval_SaveThread())
{
}
~PyRelinquishGIL()
{
PyEval_RestoreThread(_thread_state);
}
PyRelinquishGIL(const PyLockGIL&) = delete;
PyRelinquishGIL& operator=(const PyLockGIL&) = delete;
PyThreadState* _thread_state;
};

我们的代码是多线程的,这种方法运行良好。