我正在用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);
}
我的代码没有做任何与线程相关的事情,尽管我已经尝试了许多其他的东西。例如,根据this,我尝试调用PyEval_InitThreads()
,但是对于扩展应该调用的地方并不明显。我把它放在PyMODINIT_FUNC
中。这些尝试都会导致Python出现死锁,崩溃或神秘的致命错误,例如 PyEval_ReleaseThread:错误的线程状态。
这是在Linux上使用Python 3.6.1。任何想法我怎么能得到这个"简单"回电上班?
我没有意识到在另一个线程中,库在忙/等待循环中等待回调的线程。在gdb
中,info threads
使这一点显而易见。我能看到的唯一解决方案是跳过对回调的特定调用;在繁忙/等待循环的情况下,我没有找到使它们安全的方法。在这种情况下,这是可以接受的,并且这样做可以消除死锁。
此外,我似乎还需要在此之前调用PyEval_InitThreads()
。在C ++扩展中,它不清楚应该去哪里。其中一个回复建议通过创建和删除一次性threading.Thread
来间接地在Python中进行。这似乎没有解决它,而是触发了一个致命Python错误:take_gil:NULL tstate ,我认为这意味着它仍然没有GIL。我的猜测,基于this及其引用的问题,是PyEval_InitThreads()
导致当前线程成为GIL的主线程。如果在短暂的一次性线程中进行该调用,那么这可能是个问题。是的,我只是在猜测,并且会感谢那些不必做的人的解释。
答案 0 :(得分:3)
这个答案仅适用于Python> = 3.0.0。我不知道它是否适用于早期的蟒蛇。
将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()
不成立,你的模块初始化函数应该由唯一的Python线程执行,那么保持GIL是正确的。
这些是我的猜测。我从来没有做过这样的事情,就像我对你的问题的无能评论所证明的那样。但是从我对文档的阅读中,这两种方法都应该有效。
答案 1 :(得分:2)
我是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函数的单调乏味。
希望这有帮助!
答案 2 :(得分:0)
我用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;
};
我们的代码是多线程的,这种方法效果很好。