PyGILState_Ensure()导致死锁

时间:2017-11-07 20:54:10

标签: python c++ multithreading deadlock

我正在用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的主线程。如果在短暂的一次性线程中进行该调用,那么这可能是个问题。是的,我只是在猜测,并且会感谢那些不必做的人的解释。

3 个答案:

答案 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;
};

我们的代码是多线程的,这种方法效果很好。