在使用pybind11将Python解释器嵌入多线程C++程序中

5
我正在尝试使用pybind11,以便使第三方C++库调用Python方法。该库是多线程的,每个线程都会创建一个Python对象,然后对对象的方法进行多次调用。
我的问题是调用py::gil_scoped_acquire acquire;会死锁。下面是一个最小化的代码,可以重现这个问题。我做错了什么?
// main.cpp
class Wrapper
{
public:
  Wrapper()
  {
    py::gil_scoped_acquire acquire;
    auto obj = py::module::import("main").attr("PythonClass")();
    _get_x = obj.attr("get_x");
    _set_x = obj.attr("set_x");
  }
  
  int get_x() 
  {
    py::gil_scoped_acquire acquire;
    return _get_x().cast<int>();
  }

  void set_x(int x)
  {
    py::gil_scoped_acquire acquire;
    _set_x(x);
  }

private:
  py::object _get_x;
  py::object _set_x;
};


void thread_func()
{
  Wrapper w;

  for (int i = 0; i < 10; i++)
  {
    w.set_x(i);
    std::cout << "thread: " << std::this_thread::get_id() << " w.get_x(): " << w.get_x() << std::endl;
    std::this_thread::sleep_for(100ms);    
  }
}

int main() {
  py::scoped_interpreter python;
  
  std::vector<std::thread> threads;

  for (int i = 0; i < 5; ++i)
    threads.push_back(std::thread(thread_func));

  for (auto& t : threads)
    t.join();

  return 0;
}

和Python代码:

// main.py
class PythonClass:
    def __init__(self):
        self._x = 0

    def get_x(self):
        return self._x

    def set_x(self, x):
        self._x = x

相关问题可以在这里这里找到,但是并没有帮助我解决问题。


我曾经遇到过类似的问题,并在此处解决:https://dev59.com/r7noa4cB1Zd3GeqPRHrK。如果有帮助,请参考;但仔细想想,也许并不适用于你的问题,因为你的问题是相反的:从C++中运行Python代码。 - pptaszni
你正在编写什么类型的应用程序?你使用了哪个第三方库? - Basile Starynkevitch
3个回答

5

我通过在主线程中释放GIL(添加了py::gil_scoped_release release;)来解决了这个问题,然后再启动工作线程。对于任何有兴趣的人,以下方法现在可用(同时还添加了清理Python对象):

#include <pybind11/embed.h>  
#include <iostream>
#include <thread>
#include <chrono>
#include <sstream>

namespace py = pybind11;
using namespace std::chrono_literals;

class Wrapper
{
public:
  Wrapper()
  {
    py::gil_scoped_acquire acquire;
    _obj = py::module::import("main").attr("PythonClass")();
    _get_x = _obj.attr("get_x");
    _set_x = _obj.attr("set_x");

  }
  
  ~Wrapper()
  {
    _get_x.release();
    _set_x.release();
  }

  int get_x() 
  {
    py::gil_scoped_acquire acquire;
    return _get_x().cast<int>();
  }

  void set_x(int x)
  {
    py::gil_scoped_acquire acquire;
    _set_x(x);
  }

private:
  py::object _obj;
  py::object _get_x;
  py::object _set_x;
};


void thread_func(int iteration)
{
  Wrapper w;

  for (int i = 0; i < 10; i++)
  {
    w.set_x(i);
    std::stringstream msg;
    msg << "iteration: " << iteration << " thread: " << std::this_thread::get_id() << " w.get_x(): " << w.get_x() << std::endl;
    std::cout << msg.str();
    std::this_thread::sleep_for(100ms);    
  }
}

int main() {
  py::scoped_interpreter python;
  py::gil_scoped_release release; // add this to release the GIL

  std::vector<std::thread> threads;
  
  for (int i = 0; i < 5; ++i)
    threads.push_back(std::thread(thread_func, 1));

  for (auto& t : threads)
    t.join();

  return 0;
}

1

和@bavaza的答案相关,有一种方法可以将初始化和GIL释放封装到一个类中。需要注意的是,该类现在是单例模式(与scoped_interpreter没有区别),但这是可能的。以下是具体思路:

#include <pybind11/embed.h>
#include <memory>

using py = pybind11;

class PythonWrapper {
public:
    PythonWrapper() : m_interpreter() {
        // Do whatever one-time module/object initialization you want here
        py::object obj = py::module::import("main").attr("PythonClass")();  // Speeds up importing later
        
        // Last line of constructor releases the GIL
        mp_gil_release = std::make_unique<py::gil_scoped_release>();
    }
private:
    py::scoped_interpreter m_interpreter;

    // Important that this is the LAST member, so it gets destructed first, re-acquiring the GIL
    std::unique_ptr<py::gil_scoped_release> mp_gil_release;
};

这将替换在main中堆栈上的两个对象,同时保持Wrapper类不变!如果您想为所有Python调用创建真正的单例,这也会有所帮助。

再次感谢@bavaza提供的原始解决方案。它帮助我理解了如何正确地使用作用域锁来处理自己跨线程的使用。


0

Python 被认为有一个全局解释器锁(GIL)

因此,你基本上需要从头开始编写自己的 Python 解释器,或者下载 Python 的源代码并对其进行大量改进。

如果你在 Linux 上,可以考虑运行许多 Python 解释器(使用适当的系统调用(2),使用 管道(7)Unix(7) 进行 进程间通信)- 也许一个 Python 进程与你的每个 C++ 线程通信。

我做错了什么?

用Python编写一些本应该用其他语言编写的东西。你考虑过尝试SBCL吗?

一些库(例如Tensorflow)可以从Python和C++中调用。也许你可以从它们中获得灵感...

实际上,如果你在一台强大的Linux机器上只有十几个C++线程,你可以为每个C++线程承担一个Python 进程。因此,每个C++线程都将有自己的伴侣Python进程。

否则,预算几年时间来改进Python源代码以消除其GIL。你可以编写GCC插件来帮助你完成这项任务-分析和理解Python的C代码。


谢谢@Basile。我知道GIL及其限制。不幸的是,我已经有一个大型的Python代码库,现在无法移植它。 - bavaza
预算至少数月的工作时间,甚至可能需要数年。如果允许,可以使用操作系统特定的API。 - Basile Starynkevitch

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接