Einbetten eines Python-Interpreters in ein C ++ - Multithread-Programm mit pybind11

Dec 18 2020

Ich versuche, pybind11 zu verwenden, damit eine C ++ - Bibliothek eines Drittanbieters eine Python-Methode aufruft . Die Bibliothek ist Multithread-fähig, und jeder Thread erstellt ein Python-Objekt und führt dann zahlreiche Aufrufe der Methoden des Objekts durch.

Mein Problem ist, dass der Anruf zu py::gil_scoped_acquire acquire;Deadlocks. Ein minimaler Code, der das Problem reproduziert, ist unten angegeben. Was mache ich falsch?

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

und der Python-Code:

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

Verwandte Fragen finden Sie hier und hier , haben mir aber nicht geholfen, das Problem zu lösen.

Antworten

3 bavaza Dec 20 2020 at 18:37

Ich habe es geschafft, das Problem zu lösen, indem ich die GIL im Hauptthread freigegeben habe, bevor ich die Arbeitsthreads gestartet habe (hinzugefügt py::gil_scoped_release release;). Für alle Interessierten funktioniert jetzt Folgendes (auch das Bereinigen von Python-Objekten wurde hinzugefügt):

#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

Python verfügt bekanntermaßen über eine globale Interpreter-Sperre .

Sie müssen also grundsätzlich Ihren eigenen Python-Interpreter von Grund auf neu schreiben oder den Quellcode von Python herunterladen und erheblich verbessern.

Wenn Sie unter Linux arbeiten, können Sie viele Python-Interpreter ausführen (unter Verwendung geeigneter Syscalls (2) , mit Pipe (7) oder Unix (7) für die Interprozesskommunikation ) - möglicherweise einen Python-Prozess, der mit jedem Ihrer C ++ - Threads kommuniziert.

Was mache ich falsch?

Codierung in Python etwas, das sonst codiert werden sollte. Haben Sie darüber nachgedacht, SBCL auszuprobieren ?

Einige Bibliotheken (z. B. Tensorflow ) können sowohl von Python als auch von C ++ aufgerufen werden. Vielleicht könnten Sie sich von ihnen inspirieren lassen ...

In der Praxis können Sie es sich leisten, einen Python- Prozess pro C ++ - Thread zu haben, wenn Sie nur ein Dutzend C ++ - Threads auf einem leistungsstarken Linux-Computer haben . Jeder C ++ - Thread hätte also seinen eigenen begleitenden Python-Prozess.

Andernfalls sollten Sie mehrere Jahre Arbeit einplanen, um den Quellcode von Python zu verbessern und dessen GIL zu entfernen. Sie können Ihr GCC-Plugin codieren , um Ihnen bei dieser Aufgabe zu helfen - das Analysieren und Verstehen des C-Codes von Python.