PyGILState_Ensure()导致死亡

2024-04-20 00:07:11 发布

您现在位置:Python中文网/ 问答频道 /正文

我在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:error thread state。在

这是在安装了python3.6.1的Linux上。有什么办法让这个“简单”的回调生效吗?在

可能的罪魁祸首

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

而且,在这之前我似乎还需要调用PyEval_InitThreads()。在C++扩展中,不清楚应该在哪里进行。其中一个回复建议在Python中通过创建和删除一个throwaway threading.Thread来间接地执行此操作。这似乎并没有修复它,而是触发了一个致命的Python错误:以_gil:NULL tstate为例,我认为这意味着仍然没有gil。根据this及其所指的问题,我的猜测是PyEval_InitThreads()导致当前线程成为GIL的主线程。如果这个调用是在短暂的一次性线程中发出的,那么这可能是个问题。是的,我只是在猜测,希望你能给我一个解释。在


Tags: 方法函数错误resultthis事情线程pyobject
3条回答

我是StAdvOp溢出的新手,但我最近几天一直在将Python嵌入到多线程C++系统中,并遇到了代码死锁的情况。以下是我用来确保线程安全的解决方案:

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将导致线程在等待互斥时阻塞。在

使用这些函数,我相信您的回调函数如下所示:

^{pr2}$

如果您不相信您的代码可能需要多次对Python的多线程调用,那么可以轻松地扩展宏并从类结构中取出静态变量。这就是我如何处理一个未知线程,启动并确定它是否需要启动系统,并避免重复编写GIL函数的单调乏味。在

希望这有帮助!在

这个答案只适用于Python>;=3.0.0。我不知道这对早期的Python是否有效。在

在类似于此的Python模块中封装C++模块:

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

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

我不太相信这一点,但您的module init函数可以这样做:

^{pr2}$

这应该是可行的,因为如果PyEval_ThreadsInitialized()不是真的话,那么您的module init函数应该由唯一存在的Python线程执行,并且保持GIL是正确的做法。在

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

我用Python包了C++观察器。如果使用的是boost,那么可以在boost_PYTHON_模块中调用PyEval_InitThreads():

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

然后我用类来控制从C++调用回Python。在

^{pr2}$

如果你调用C++的任何时间长度,你也可以放弃吉尔:

struct PyRelinquishGIL
{
    PyRelinquishGIL()
        : _thread_state(PyEval_SaveThread())
    {
    }
    ~PyRelinquishGIL()
    {
        PyEval_RestoreThread(_thread_state);
    }

    PyRelinquishGIL(const PyLockGIL&) = delete;
    PyRelinquishGIL& operator=(const PyLockGIL&) = delete;

    PyThreadState* _thread_state;
};

我们的代码是多线程的,这种方法工作得很好。在

相关问题 更多 >