Boost Python,传播c++回调到Python导致分段错误

Boost Python, propagate C++ callbacks to Python causing segmentation fault

本文关键字:Python 错误 分段 回调 传播 c++ Boost      更新时间:2023-10-16

我在c++中有以下侦听器,它接收一个Python对象来传播回调。

class PyClient {
    private:
        std::vector<DipSubscription *> subs;
        subsFactory *sub;
        class GeneralDataListener: public SubscriptionListener {
            private:
                PyClient * client;
            public:
                GeneralDataListener(PyClient *c):client(c){
                    client->pyListener.attr("log_message")("Handler created");
                }
                void handleMessage(Subscription *sub, Data &message) {
                    // Lock the execution of this method
                    PyGILState_STATE state = PyGILState_Ensure();
                    client->pyListener.attr("log_message")("Data received for topic");
                    ...
                    // This method ends modifying the value of the Python object
                    topicEntity.attr("save_value")(valueKey, extractDipValue(valueKey.c_str(), message))
                    // Release the lock
                    PyGILState_Release(state);
                }
                void connected(Subscription *sub) {
                    client->pyListener.attr("connected")(sub->getTopicName());
                }
                void disconnected(Subscription *sub, char* reason) {
                    std::string s_reason(reason);
                    client->pyListener.attr("disconnected")(sub->getTopicName(), s_reason);
                }
                void handleException(Subscription *sub, Exception &ex) {
                    client->pyListener.attr("handle_exception")(sub->getTopicName())(ex.what());
                }
        };
        GeneralDataListener *handler;
    public:
        python::object pyListener;

        PyClient(python::object pyList): pyListener(pyList) {
            std::ostringstream iss;
            iss << "Listener" << getpid();
            sub = Sub::create(iss.str().c_str());
            createSubscriptions();
        }
        ~PyClient() {
            for (unsigned int i = 0; i < subs.size(); i++) {
                if (subs[i] == NULL) {
                    continue;
                }
                sub->destroySubscription(subs[i]);
            }
        }
};

BOOST_PYTHON_MODULE(pytest)
{
    // There is no need to expose more methods as will be used as callbacks
    Py_Initialize();
    PyEval_InitThreads();
    python::class_<PyClient>("PyClient",     python::init<python::object>())
        .def("pokeHandler", &PyClient::pokeHandler);
};

然后,我有了我的Python程序,就像这样:

import sys
import time
import pytest

class Entity(object):
    def __init__(self, entity, mapping):
        self.entity = entity
        self.mapping = mapping
        self.values = {}
        for field in mapping:
            self.values[field] = ""
        self.updated = False
    def save_value(self, field, value):
        self.values[field] = value
        self.updated = True

class PyListener(object):
    def __init__(self):
        self.listeners = 0
        self.mapping = ["value"]
        self.path_entity = {}
        self.path_entity["path/to/node"] = Entity('Name', self.mapping)
    def connected(self, topic):
        print "%s topic connected" % topic
    def disconnected(self, topic, reason):
        print "%s topic disconnected, reason: %s" % (topic, reason)
    def handle_message(self, topic):
        print "Handling message from topic %s" % topic
    def handle_exception(self, topic, exception):
        print "Exception %s in topic %s" % (exception, topic)
    def log_message(self, message):
       print message
    def sample(self):
        for path, entity in self.path_entity.iteritems():
            if not entity.updated:
                return False
            sample = " ".join([entity.values[field] for field in dip_entity.mapping])
            print "%d %s %d %s" % (0, entity.entity, 4324, sample)
            entity.updated = False
        return True

if __name__ == "__main__":
    sys.settrace(trace)
    py_listener = PyListener()
    sub = pytest.PyClient(py_listener)
    while True:
        if py_listener.sample():
            break

所以,最后,我的问题似乎是,当我开始在Python程序中运行while True时,脚本会卡住检查实体是否更新,并且随机地,当c++侦听器试图调用回调时,我得到一个分段错误。

如果我只是尝试时间也是一样的。在python脚本中休眠并按时间调用sample。我知道如果我从c++代码中调用sample就可以解决这个问题,但是这个脚本将由其他Python模块运行,这些模块将调用给定特定延迟的sample方法。因此,预期的功能将是c++更新实体的值,而Python脚本只读取它们。

我已经用gdb调试了这个错误,但是我得到的堆栈跟踪并没有太多的解释:

#0  0x00007ffff7a83717 in PyFrame_New () from /lib64/libpython2.7.so.1.0
#1  0x00007ffff7af58dc in PyEval_EvalFrameEx () from /lib64/libpython2.7.so.1.0
#2  0x00007ffff7af718d in PyEval_EvalCodeEx () from /lib64/libpython2.7.so.1.0
#3  0x00007ffff7af7292 in PyEval_EvalCode () from /lib64/libpython2.7.so.1.0
#4  0x00007ffff7b106cf in run_mod () from /lib64/libpython2.7.so.1.0
#5  0x00007ffff7b1188e in PyRun_FileExFlags () from /lib64/libpython2.7.so.1.0
#6  0x00007ffff7b12b19 in PyRun_SimpleFileExFlags () from /lib64/libpython2.7.so.1.0
#7  0x00007ffff7b23b1f in Py_Main () from /lib64/libpython2.7.so.1.0
#8  0x00007ffff6d50af5 in __libc_start_main () from /lib64/libc.so.6
#9  0x0000000000400721 in _start ()

如果用sys调试。在Python中trace分段错误前的最后一行总是在样例方法中,但它可能会有所不同。

我不知道如何解决这个沟通问题,所以任何正确方向的建议都将不胜感激。

编辑修改PyDipClient对PyClient的引用

正在发生的事情是我从Python主方法启动程序,如果然后c++侦听器尝试回调Python侦听器,它会因分割错误错误而崩溃,我认为创建的唯一线程是当我创建订阅时,但那是来自库内部的代码,我不知道是如何工作的。

如果我删除Python侦听器的所有回调,并强制Python的方法(如调用pokehandler),一切都工作得很好。

最可能的罪魁祸首是线程在调用Python代码时没有持有全局解释器锁(GIL),从而导致未定义的行为。在调用Python代码之前,验证所有进行Python调用的路径(例如GeneralDataListener的函数)是否获得GIL。如果正在复制PyClient,则需要以允许GIL在复制和销毁时保持的方式管理pyListener

进一步,考虑PyClient的三规则。复制构造函数和赋值操作符需要对订阅做任何事情吗?


GIL是CPython解释器周围的互斥对象。这个互斥锁防止在Python对象上执行并行操作。因此,在任何时间点,最多只允许一个线程(即获得GIL的线程)对Python对象执行操作。当存在多个线程时,在不持有GIL的情况下调用Python代码会导致未定义的行为。

在Python文档中,

C或c++线程有时被称为外来线程。Python解释器无法控制外来线程。因此,外部线程负责管理GIL,以允许Python线程并发或并行执行。

当前代码中:

  • GeneralDataListener::handle_message()以非异常安全的方式管理GIL。例如,如果侦听器的log_message()方法抛出异常,则堆栈将展开而不会释放GIL,因为PyGILState_Release()将不会被调用。

    void handleMessage(...)
    {
      PyGILState_STATE state = PyGILState_Ensure();
      client->pyListener.attr("log_message")(...);
      ...
      PyGILState_Release(state); // Not called if Python throws.
    }
    
  • GeneralDataListener::connected(), GeneralDataListener:: disconnected()GeneralDataListener:: handleException()显式调用Python代码,但不显式管理GIL。如果调用者不拥有GIL,则在没有GIL的情况下执行Python代码时调用未定义行为。

    void connected(...)
    {
      // GIL not being explicitly managed.
      client->pyListener.attr("connected")(...);
    }
    
  • PyClient隐式创建的复制构造函数和赋值操作符不管理GIL,但在复制pyListener数据成员时可能间接调用Python代码。如果正在进行复制,那么调用方需要在复制和销毁PyClient::pyListener对象时持有GIL。如果pyListener不在空闲空间上管理,那么调用者必须是Python感知的,并且在销毁整个PyClient对象期间获得了GIL。

要解决这些问题,请考虑:
  • 使用资源获取初始化(RAII)保护类以异常安全的方式帮助管理GIL。例如,对于下面的gil_lock类,当创建gil_lock对象时,调用线程将获取GIL。当gil_lock对象被析构时,它释放GIL

    /// @brief RAII class used to lock and unlock the GIL.
    class gil_lock
    {
    public:
      gil_lock()  { state_ = PyGILState_Ensure(); }
      ~gil_lock() { PyGILState_Release(state_);   }
    private:
      PyGILState_STATE state_;
    };
    ...
    void handleMessage(...)
    {
      gil_lock lock;
      client->pyListener.attr("log_message")(...);
      ...
    }
    
  • 在任何从外部线程调用Python代码的代码路径中显式地管理GIL。

    void connected(...)
    {
      gil_lock lock;
      client->pyListener.attr("connected")(...);
    }
    
  • 使PyClient不可复制或显式创建复制构造函数和赋值操作符。如果正在进行复制,则将pyListener更改为允许在保存GIL时显式销毁的类型。一种解决方案是使用boost::shared_ptr<python::object>来管理在构建过程中提供给PyClientpython::object的副本,并具有一个能够感知GIL的自定义删除程序。或者,也可以使用boost::optional .

    class PyClient
    {
    public:
      PyClient(const boost::python::object& object)
        : pyListener(
            new boost::python::object(object),  // GIL locked, so copy.
            [](boost::python::object* object)   // Delete needs GIL.
            {
              gil_lock lock;
              delete object;
            }
          )
      {
        ...
      }
    private:
      boost::shared_ptr<boost::python::object> pyListener;;
    };
    

    注意,通过管理自由空间上的boost::python::object,可以自由地复制shared_ptr,而无需持有GIL。另一方面,如果使用boost::optional之类的东西来管理Python对象,则需要在复制构造、赋值和销毁期间持有GIL。

考虑阅读这个答案,了解更多关于Python回调的细节和微妙的细节,例如复制构造和销毁期间的GIL管理。