【问题标题】:PyGILState_Ensure() Causing DeadlockPyGILState_Ensure() 导致死锁
【发布时间】:2018-04-20 09:43:08
【问题描述】:

我正在用 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: wrong thread state

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

可能的罪魁祸首

我没有意识到在另一个线程中,库处于忙/等待循环中,等待回调线程。在gdbinfo threads 中,这一点显而易见。我能看到的唯一解决方案是跳过那些对回调的特定调用;鉴于繁忙/等待循环,我看不到让它们安全的方法。在这种情况下,这是可以接受的,这样做可以消除死锁。

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

【问题讨论】:

  • 从线程中执行此操作 Python 一无所知,这不是一种非常有效的方法。部分原因是 Python 开始对其控制的线程进行非常严格的管理,以确保一次只有一个线程在运行。
  • 我建议有一个队列,其中一个端点是一个 Python 线程,它位于一个紧密的循环中,并在队列中抓取一个信号量,其中包含一些东西。然后它从队列中拉取东西并执行回调函数。
  • 哪个版本的 Python?
  • 这是在 Linux 上使用 Python 3.6.1。基于this,我得出结论 Python 3 可以使用它没有创建的线程。
  • 你说得对,从PyGILState_Ensure 的文档中可以清楚地看出,它正是为这种情况而设计的,因为 Python 使用它没有创建的线程。

标签: python c++ multithreading deadlock


【解决方案1】:

我在 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;
};

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

【讨论】:

    【解决方案2】:

    我是 StackOverflow 的新手,但过去几天我一直致力于将 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 将导致线程在等待其互斥体时变砖。

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

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

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

    希望这会有所帮助!

    【讨论】:

      【解决方案3】:

      此答案仅适用于 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 是正确的做法。

      这些都是我的猜测。我从来没有做过这样的事情,我对你的问题一无所知的cmets就证明了这一点。但是根据我对文档的阅读,这两种方法都应该有效。

      【讨论】:

      • 我将它添加到模块的__init__.py,但我仍然死锁。发帖前,我也试过PyEval_InitThreads()等电话。我认为您的threading.Thread 想法是等效的。
      • 查看 Python 源代码,假设我理解我在那里看到的内容,PyEval_InitThreads() 在道德上等同于PyEval_ThreadsInitialized(),所以没有必要这么多。另外,我正在用我认为可能的罪魁祸首来编辑我的答案。简而言之,我没有意识到图书馆在另一个线程中正在等待这个线程。
      • @Jim - 如果我现在没有得到报酬的任务,我会深入研究并弄清楚到底发生了什么,从尝试复制开始你的问题是一个非常简单的模块。 :-) 也许你可以尝试这种方法。创建一个简单的模块,该模块具有一个接受回调的函数,然后启动另一个每秒调用回调的线程。如果这有效,那将是一个巨大的线索。如果没有,就更容易找出原因。
      • @Jim - 哦,有趣!我有点猜测它一定是关于您的程序如何使用线程的特定内容。
      猜你喜欢
      • 2012-09-20
      • 2018-06-04
      • 2016-12-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-03-03
      相关资源
      最近更新 更多