提问人:pptaszni 提问时间:2/26/2020 更新时间:8/19/2020 访问量:5538
如何使用 pybind11 在 C++ 线程中调用 Python 函数作为回调
How to invoke Python function as a callback inside C++ thread using pybind11
问:
我设计了一个 C++ 系统,该系统从在单独线程中运行的过程调用用户定义的回调。简化如下所示:system.hpp
#pragma once
#include <atomic>
#include <chrono>
#include <functional>
#include <thread>
class System
{
public:
using Callback = std::function<void(int)>;
System(): t_(), cb_(), stop_(true) {}
~System()
{
stop();
}
bool start()
{
if (t_.joinable()) return false;
stop_ = false;
t_ = std::thread([this]()
{
while (!stop_)
{
std::this_thread::sleep_for(std::chrono::milliseconds(100));
if (cb_) cb_(1234);
}
});
return true;
}
bool stop()
{
if (!t_.joinable()) return false;
stop_ = true;
t_.join();
return true;
}
bool registerCallback(Callback cb)
{
if (t_.joinable()) return false;
cb_ = cb;
return true;
}
private:
std::thread t_;
Callback cb_;
std::atomic_bool stop_;
};
它工作得很好,可以用这个简短的例子进行测试:main.cpp
#include <iostream>
#include "system.hpp"
int g_counter = 0;
void foo(int i)
{
std::cout << i << std::endl;
g_counter++;
}
int main()
{
System s;
s.registerCallback(foo);
s.start();
while (g_counter < 3)
{
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
s.stop();
return 0;
}
这将输出几次,然后停止。但是,我在尝试为我的 .如果我注册一个 python 函数作为回调,我的程序会在调用 后死锁。我稍微调查了一下这个话题,似乎我遇到了 GIL 的问题。可重现的例子:1234
System
System::stop
binding.cpp
:
#include "pybind11/functional.h"
#include "pybind11/pybind11.h"
#include "system.hpp"
namespace py = pybind11;
PYBIND11_MODULE(mysystembinding, m) {
py::class_<System>(m, "System")
.def(py::init<>())
.def("start", &System::start)
.def("stop", &System::stop)
.def("registerCallback", &System::registerCallback);
}
python脚本:
#!/usr/bin/env python
import mysystembinding
import time
g_counter = 0
def foo(i):
global g_counter
print(i)
g_counter = g_counter + 1
s = mysystembinding.System()
s.registerCallback(foo)
s.start()
while g_counter < 3:
time.sleep(1)
s.stop()
我已经阅读了pybind11文档部分,了解在C++端获取或发布GIL的可能性。但是,我没有设法摆脱在我的情况下发生的僵局:
PYBIND11_MODULE(mysystembinding, m) {
py::class_<System>(m, "System")
.def(py::init<>())
.def("start", &System::start)
.def("stop", &System::stop)
.def("registerCallback", [](System* s, System::Callback cb)
{
s->registerCallback([cb](int i)
{
// py::gil_scoped_acquire acquire;
// py::gil_scoped_release release;
cb(i);
});
});
}
如果我在调用回调之前调用,无论如何都会发生死锁。
如果我在调用回调之前调用,我会得到py::gil_scoped_acquire acquire;
py::gil_scoped_release release;
致命的 Python 错误:PyEval_SaveThread:NULL tstate
如何将python函数注册为回调,避免死锁?
答:
5赞
pptaszni
2/27/2020
#1
多亏了这个讨论和许多其他资源(1、2、3),我发现保护启动和加入 C++ 线程的函数似乎可以解决问题:gil_scoped_release
PYBIND11_MODULE(mysystembinding, m) {
py::class_<System>(m, "System")
.def(py::init<>())
.def("start", &System::start, py::call_guard<py::gil_scoped_release>())
.def("stop", &System::stop, py::call_guard<py::gil_scoped_release>())
.def("registerCallback", &System::registerCallback);
}
显然,死锁的发生是因为 python 在调用负责 C++ 线程操作的绑定时持有锁。我仍然不确定我的推理是否正确,所以我将不胜感激任何专家的评论。
评论
0赞
cade
9/13/2020
难道您不必在回调中获取 GIL,因为当您在工作线程中调用它时,它已经释放了吗?还是由 Pybind 在类型转换中自动完成?
0赞
pptaszni
9/14/2020
我认为如果回调是 python 函数,则不必这样做。在此示例中,如果 C++ 函数在内部调用一些 python 代码,它们会获取锁,这里我遇到了相反的情况。但老实说,我不太明白他们在这个例子中到底在保护什么,因为我在那里没有看到任何并行代码。
3赞
kuokuo
8/19/2020
#2
在我的情况下,打电话之前将摆脱僵局。gil_scoped_release
join()
void Tick::WaitLifeOver() {
if (thread_.joinable()) {
thread_.join();
}
}
PYBIND11_MODULE(tick_pb, m) {
py::class_<Tick, std::shared_ptr<Tick>>(m, "Tick")
// ...
.def("wait_life_over", &Tick::WaitLifeOver,
py::call_guard<py::gil_scoped_release>());
}
代码如下: C++ 线程回调 Python 函数
评论
0赞
pptaszni
8/20/2020
谢谢你的回答。但是,请注意,您的解决方案与我 6 个月前发布的解决方案相同。无论如何,很高兴确认它也适合您。
评论