Boost Python,将C ++回调传播给Python导致分段错误

时间:2015-07-22 07:32:42

标签: python c++ callback boost-python

我在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时,脚本会卡住检查实体是否更新,并随机地,当C ++侦听器尝试调用回调时,我得到了一个分段错误。

如果我只是在python脚本中尝试time.sleep并按时间调用样本,那就相同。我知道如果我从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 ()

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

我不确定如何解决这些沟通问题,因此我们非常感谢任何正确方向的建议。

修改 将PyDipClient引用修改为PyClient。

正在发生的事情是我从Python main方法启动程序,如果C ++监听器尝试回调Python监听器它会因分段错误错误而崩溃,我认为唯一创建的线程就是当我创建订阅时,但那是来自图书馆内部的代码,我不知道它是如何正常工作的。

如果我删除Python侦听器的所有回调,并强制Python中的方法(比如调用pokehandler),一切都运行良好。

1 个答案:

答案 0 :(得分:2)

最可能的罪魁祸首是,当调用Python代码时,线程不会保留Global Interpreter Lock(GIL),从而导致未定义的行为。验证所有进行Python调用的路径(例如GeneralDataListener的函数)在调用Python代码之前获取GIL。如果正在制作PyClient的副本,则需要以允许GIL在复制和销毁时保留的方式管理pyListener

此外,请考虑PyClient的{​​{3}}。复制构造函数和赋值运算符是否需要对订阅执行任何操作?

GIL是CPython解释器周围的互斥体。此互斥锁可防止对Python对象执行并行操作。因此,在任何时间点,允许最多一个线程(已获取GIL的线程)对Python对象执行操作。当存在多个线程时,调用Python代码而不保存GIL会导致未定义的行为。

C或C ++线程有时在Python文档中称为外来线程。 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。

要解决这些问题,请考虑:

  • 使用rule of three(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>来管理在构建期间提供给python::object的{​​{1}}的副本,并且具有可识别GIL的自定义删除器。或者,可以使用类似Resource Acquisition Is Initialization的内容。

    PyClient

    请注意,通过管理可用空间上的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;; }; ,可以在不保留GIL的情况下自由复制boost::python::object。另一方面,如果使用shared_ptr之类的东西来管理Python对象,则需要在复制构造,赋值和销毁期间保留GIL。

考虑阅读boost::optional答案,了解有关回调到Python的更多细节和细微的细节,例如复制构建和销毁期间的GIL管理。