pybind11を使用したマルチスレッドC ++プログラムへのPythonインタープリターの埋め込み
サードパーティのC ++ライブラリにPythonメソッドを呼び出させるために、pybind11を使用しようとしています。ライブラリはマルチスレッド化されており、各スレッドは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
関連する質問はこことここにありますが、問題の解決には役立ちませんでした。
回答
ワーカースレッドを開始する前に、メインスレッドで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;
}
Pythonにはグローバルインタープリターロックがあることが知られています。
したがって、基本的には、独自のPythonインタープリターを最初から作成するか、Pythonのソースコードをダウンロードして大幅に改善する必要があります。
Linuxを使用している場合は、多くのPythonインタープリターを実行することを検討できます(プロセス間通信に適切なsyscalls(2)を使用し、pipe(7)またはunix(7)を使用します)-おそらく1つのPythonプロセスが各C ++スレッドと通信します。
私は何が間違っているのですか?
Pythonでコーディングする場合は、別の方法でコーディングする必要があります。SBCLを試すことを検討しましたか?
一部のライブラリ(Tensorflowなど)は、PythonとC ++の両方から呼び出すことができます。多分あなたはそれらからインスピレーションを得ることができます...
実際には、強力なLinuxマシンにC ++スレッドが12個しかない場合は、C ++スレッドごとに1つのPythonプロセスを使用する余裕があります。したがって、各C ++スレッドには独自のコンパニオンPythonプロセスがあります。
それ以外の場合は、Pythonのソースコードを改善してGILを削除するために、数年の作業の予算を立ててください。あなたはあなたのコードかもしれないGCCのプラグインを、そのタスクが-analyzingとPythonのCコードを理解する上であなたを助けるために。