我在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),一切都运行良好。
答案 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管理。