Boost Python,传播c++回调到Python导致分段错误
Boost Python, propagate C++ callbacks to Python causing segmentation fault
我在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>
来管理在构建过程中提供给PyClient
的python::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管理。
- 无法使用 SWIG 在 Python 中实例化C++类(获取属性错误)
- 为什么PyImport_ImportModule python 3.7.2 中出现段错误?
- OpenCV - Python 断言错误:SAD 算法 - 立体相机视差图计算
- 如何在 Python C++ 混合库中调试非确定性分段错误?
- 为 Python 构建共享库C++时出现分段错误
- NS3 - python.h 文件无法定位编译错误
- Python os.system() 返回错误值
- 致命的 Python 错误:无法获取随机数来初始化 Python
- 使用 Pybind11 将函数集成到 Python C++错误,同时使用 PYBIND11_MODULE
- Python 错误:在 SWIG 生成的C++模板化代码模块中没有定义构造函数
- 致命的Python错误:Py_Initialize:无法加载文件系统编解码器&ModuleNotFoundError
- Python 错误:PyThreadState_Get:编译扩展后没有当前线程
- “致命的 Python 错误:PyThreadState_Get:没有当前线程”是什么意思
- 使用 IPython 嵌入 Python 错误: [错误 193] %1 不是有效的 Win32 应用程序
- 致命 Python 错误:无法使用C++绑定创建自动 TLSkey 映射
- 如何使用Boost获取SyntaxError、NameError等的Python错误信息
- 为什么PyGILState_Release抛出致命的 Python 错误
- 由python导入的python错误
- helloworld示例中的Boost.python错误
- 不支持嵌入 python 错误按文件名导入