Встраивание интерпретатора Python в многопоточную программу на C++ с помощью pybind11

Я пытаюсь использовать 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

Связанные вопросы можно найти здесь и здесь, но не помогли мне решить проблему.

У меня была похожая проблема [решена] здесь. посмотрите, поможет ли это; Во-вторых, вероятно, это не так, поскольку ваша проблема противоположна: запустить код Python из C++.

pptaszni 17.12.2020 17:10

Какое приложение вы кодируете? Какую стороннюю библиотеку вы используете?

Basile Starynkevitch 20.12.2020 09:23
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
5
2
1 825
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Python , как известно, имеет глобальную блокировку интерпретатора.

Таким образом, вам в основном нужно написать свой собственный интерпретатор Python с нуля или загрузить исходный код Python и значительно улучшить его.

Если вы работаете в Linux, вы можете рассмотреть возможность запуска многих интерпретаторов Python (используя соответствующие системные вызовы (2) , с pipe (7) или unix (7) для межпроцессного взаимодействия) - возможно, один Процесс Python взаимодействует с каждым из ваших потоков C++.

Что я делаю не так?

Кодирование на Python чего-то, что должно быть закодировано иначе. А вы не думали попробовать SBCL?

Некоторые библиотеки (например, Tensorflow) можно вызывать как из Python, так и из C++. Может быть, вы могли бы черпать вдохновение из них...

На практике, если у вас есть всего дюжина потоков C++ на мощной машине с Linux, вы можете позволить себе иметь один процесс Python на каждый поток C++. Таким образом, у каждого потока C++ будет свой собственный сопутствующий процесс Python.

В противном случае запланируйте несколько лет работы по улучшению исходного кода Python, чтобы удалить его GIL. Вы можете написать свой плагин GCC, чтобы помочь вам в решении этой задачи — анализе и понимании C-кода Python.

Спасибо @Basile. Я знаю о GIL и его ограничениях. К сожалению, у меня уже есть большая база кода на Python, и портировать ее сейчас не представляется возможным.

bavaza 20.12.2020 09:27

Бюджетные месяцы работы минимум, может несколько лет. Если разрешено, используйте специфичные для операционной системы API.

Basile Starynkevitch 20.12.2020 09:29
Ответ принят как подходящий

Мне удалось решить проблему, выпустив 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;
}

В связи с ответом @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 за оригинальное решение. Это помогло мне разобраться в правильном способе использования блокировок с ограниченной областью действия для моего собственного использования в нескольких потоках.

Другие вопросы по теме

Похожие вопросы