【问题标题】:Embedding a Python interpreter in a multi-threaded C++ program with pybind11使用 pybind11 在多线程 C++ 程序中嵌入 Python 解释器
【发布时间】:2021-03-28 08:28:08
【问题描述】:

我正在尝试使用 pybind11 以使第 3 方 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

相关问题可以找到herehere,但是没有帮我解决问题。

【问题讨论】:

  • 我遇到了类似的问题 [已解决]here。看看是否有帮助;第二个想法可能不是,因为你的问题是相反的:从 C++ 运行 python 代码。
  • 你在编写什么样的应用程序?您使用的是什么第三方库?

标签: python c++ multithreading pybind11


【解决方案1】:

已知Python 有一个Global Interpreter Lock

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

如果您在 Linux 上,您可以考虑运行许多 Python 解释器(使用适当的 syscalls(2),使用 pipe(7)unix(7) 用于 interprocess communication)——也许一个 Python 进程与您的每个 C++ 线程通信。

我做错了什么?

在 Python 中编码应该以其他方式编码的东西。你考虑过SBCL吗?

可以从 Python 和 C++ 调用某些库(例如 Tensorflow)。也许你可以从他们那里得到灵感……

实际上,如果您在一台功能强大的 Linux 机器上只有十几个 C++ 线程,那么您可以负担每个 C++ 线程有一个 Python process。所以每个 C++ 线程都会有自己的伴随 Python 进程。

否则,预算数年的工作来改进 Python 的源代码以删除其 GIL。您可以编写您的 GCC plugin 代码来帮助您完成该任务 - 分析和理解 Python 的 C 代码。

【讨论】:

  • 谢谢@Basile。我知道 GIL 及其局限性。不幸的是,我已经有一个庞大的 Python 代码库,现在移植它是不可行的。
  • 预算至少几个月的工作,也许几年。如果允许,请使用操作系统特定的 API。
【解决方案2】:

在启动工作线程之前,我通过在主线程中释放 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;
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-05-26
    • 1970-01-01
    • 2012-05-24
    • 1970-01-01
    • 2014-11-21
    • 1970-01-01
    相关资源
    最近更新 更多