Incrustar un intérprete de Python en un programa C ++ multiproceso con pybind11

Dec 18 2020

Estoy tratando de usar pybind11 para hacer que una biblioteca de C ++ de terceros llame a un método de Python. La biblioteca es multiproceso y cada subproceso crea un objeto Python y luego realiza numerosas llamadas a los métodos del objeto.

Mi problema es que la llamada a py::gil_scoped_acquire acquire;interbloqueos. A continuación se proporciona un código mínimo que reproduce el problema. ¿Qué estoy haciendo mal?

// 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;
}

y el código 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

Se pueden encontrar preguntas relacionadas aquí y aquí , pero no me ayudaron a resolver el problema.

Respuestas

3 bavaza Dec 20 2020 at 18:37

Logré resolver el problema liberando el GIL en el hilo principal, antes de iniciar los hilos de trabajo (agregado py::gil_scoped_release release;). Para cualquiera que esté interesado, lo siguiente ahora funciona (también se agregó la limpieza de objetos de 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 BasileStarynkevitch Dec 20 2020 at 15:11

Se sabe que Python tiene un bloqueo de intérprete global .

Así que básicamente necesitas escribir tu propio intérprete de Python desde cero, o descargar el código fuente de Python y mejorarlo mucho.

Si está en Linux, podría considerar ejecutar muchos intérpretes de Python (usando llamadas al sistema apropiadas (2) , con tubería (7) o unix (7) para la comunicación entre procesos ), tal vez un proceso de Python comunicándose con cada uno de sus subprocesos de C ++.

¿Qué estoy haciendo mal?

Codificar en Python algo que debería codificarse de otra manera. ¿Consideró probar SBCL ?

Algunas bibliotecas (por ejemplo, Tensorflow ) se pueden llamar tanto desde Python como desde C ++. Tal vez puedas inspirarte en ellos ...

En la práctica, si solo tiene una docena de subprocesos de C ++ en una potente máquina Linux, podría permitirse tener un proceso de Python por subproceso de C ++. Entonces, cada hilo de C ++ tendría su propio proceso de Python complementario.

De lo contrario, presupuesta varios años de trabajo para mejorar el código fuente de Python para eliminar su GIL. Puede codificar su complemento GCC para ayudarlo en esa tarea: analizar y comprender el código C de Python.