如何使用 pybind11 在 C++ 线程中调用 Python 函数作为回调

Posted

技术标签:

【中文标题】如何使用 pybind11 在 C++ 线程中调用 Python 函数作为回调【英文标题】:How to invoke Python function as a callback inside C++ thread using pybind11 【发布时间】:2020-02-26 09:01:38 【问题描述】:

我设计了一个 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;

这将输出1234 几次然后停止。但是,我在尝试为我的System 创建 python 绑定时遇到了问题。如果我注册一个python函数作为回调,我的程序在调用System::stop后会死锁。我对该主题进行了一些调查,似乎我遇到了GIL 的问题。可重现的例子:

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 docs 部分关于在 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函数注册为回调并避免死锁?

【问题讨论】:

【参考方案1】:

感谢this discussion 和许多其他资源(1、2、3)我发现用gil_scoped_release 保护启动和加入 C++ 线程的函数似乎可以解决问题:

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++ 线程操作的绑定时持有锁。我仍然不确定我的推理是否正确,所以我会感谢任何专家的 cmets。

【讨论】:

你不必在回调中获取 GIL,因为它在你在工作线程中调用它时已经释放了吗?或者这是 Pybind 在类型转换中自动完成的? 如果回调是 python 函数,我认为你不必这样做。在this example 中,如果 C++ 函数调用内部的一些 python 代码,他们会获取锁,这里我有相反的情况。但老实说,我不太明白他们在那个例子中到底在保护什么,因为我在那里看不到任何并行代码。【参考方案2】:

join() 之前调用gil_scoped_release 将摆脱我的情况的僵局。

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++ Thread Callback Python Function

【讨论】:

感谢您的回答。但是,请注意,您的解决方案与我 6 个月前发布的解决方案相同。无论如何,很高兴确认它也适用于您。

以上是关于如何使用 pybind11 在 C++ 线程中调用 Python 函数作为回调的主要内容,如果未能解决你的问题,请参考以下文章

如何通过pybind11在python中捕获C++的异常?

混合编程:如何用pybind11调用C++

windows10系统下基于pybind11库进行c++代码调用python(pytorch)代码

pybind11 以某种方式减慢了 c++ 函数

如何在pybind11中将共享指针列表传递给c++对象

在 pybind11 中引用 C++ 分配的对象