(伪)代码
这是我遇到问题的概念的不可编译代码草图:
struct Data {};
struct A {};
struct B {};
struct C {};
/* and many many more...*/
template<typename T>
class Listener {
public:
Listener(MyObject* worker):worker(worker)
{ /* do some magic to register with RTI DDS */ };
public:
// This function is used ass a callback from RTI DDS, i.e. it will be
// called from other threads when new Data is available
void callBackFunction(Data d)
{
T t = extractFromData(d);
// Option 1: direct function call
// works somewhat, but shows "QObject::startTimer: timers cannot be started
// from another thread" at the console...
worker->doSomeWorkWithData(t); //
// Option 2: Use invokeMethod:
// seems to fail, as the macro expands including '"T"' and that type isn't
// registered with the QMetaType system...
// QMetaObject::invokeMethod(worker,"doSomeGraphicsWork",Qt::AutoConnection,
// Q_ARG(T, t)
// );
// Option 3: use signals slots
// fails as I can't make Listener, a template class, a QObject...
// emit workNeedsToBeDone(t);
}
private:
MyObject* worker;
T extractFromData(Data d){ return T(d);};
};
class MyObject : public QObject {
Q_OBJECT
public Q_SLOTS:
void doSomeWorkWithData(A a); // This one affects some QGraphicsItems.
void doSomeWorkWithData(B b){};
void doSomeWorkWithData(C c){};
public:
MyObject():QObject(nullptr){};
void init()
{
// listeners are not created in the constructor, but they should have the
// same thread affinity as the MyObject instance that creates them...
// (which in this example--and in my actual code--would be the main GUI
// thread...)
new Listener<A>(this);
new Listener<B>(this);
new Listener<C>(this);
};
};
main()
{
QApplication app;
/* plenty of stuff to set up RTI DDS and other things... */
auto myObject = new MyObject();
/* stuff resulting in the need to separate "construction" and "initialization" */
myObject.init();
return app.exec();
};
实际代码中的更多细节:
Listener
示例中的RTI DataReaderListener,回调函数是onDataAvailable()
我想完成什么
我正在尝试编写一个小型分布式程序,它使用RTI 的 Connext DDS进行通信,使用Qt5进行 GUI 的东西——但是,据我所知,我认为这些细节并不重要,因为问题归结为以下:
- 我有一个 QObject 派生对象
myObject
,它的线程亲缘关系可能与主 GUI 线程相关,也可能不相关(但为简单起见,我们假设是这种情况。) - 我希望该对象对另一个非 Qt 3rd-party 库中发生的事件做出反应(在我上面的示例代码中,由 functions 表示
doSomeWorkWithData()
。
到目前为止我所理解的为什么这是有问题的
免责声明:像往常一样,在开始一个新项目时总会学到不止一件新东西。对我来说,这里的新事物是 / 是 RTI 的 Connext 和(显然)我第一次自己必须处理线程。
从阅读 Qt ( 1、2、3、4和5 ) 中的线程,在我看来
- QObjects 通常不是线程安全的,即我必须小心一些事情
- 使用与 QObjects 进行“通信”的正确方式应该可以让我避免自己处理互斥锁等,即其他人(Qt?)可以为我处理序列化访问。
因此,我不能简单地(随机)调用,MyClass::doSomeWorkWithData()
但我需要对其进行序列化。一种可能很简单的方法是将事件发布到事件队列中,在我的情况下myObject
,当时间可用时,将触发所需方法的执行。MyClass::doSomeWorkWithData()
我试图让事情发挥作用
我已经确认myObject
,当与上面的示例代码类似地实例化时,它隶属于主 GUI 线程,即myObject.thread() == QApplication::instance()->thread()
.
鉴于此,到目前为止,我已经尝试了三种选择:
方案一:直接调用函数
这种方法基于以下事实 -myObject
存在于 GUI 线程中 - 所有创建的侦听器也附属于 GUI 线程,因为它们是由“myObject”创建并以这种方式继承其线程的
这实际上导致了doSomeWorkWithData()
被执行的事实。但是,其中一些函数操纵 QGraphicsItems,每当出现这种情况时,我都会收到错误消息:“QObject::startTimer: timers cannot be started from another thread”。
选项 2:通过发布活动QMetaObject::invokeMethod()
试图通过正确发布一个事件来规避这个问题myObject
,我试图用 标记MyObject::doSomeWorkWithData()
,Q_INVOKABLE
但我未能调用该方法,因为我需要用 传递参数Q_ARG
。我struct A
在示例中正确注册并声明了由 等表示的自定义类型),但我失败了,因为
Q_ARG
扩展以包含参数类型的文字,在模板化的情况下不起作用("T"
不是注册或声明的类型)。
尝试使用传统的信号和插槽
这种方法基本上直接失败了,因为 QMeta 系统不能使用模板,即在我看来,根本不可能有任何模板化的 QObjects。
我需要什么帮助
在花了大约一周的时间试图解决这个问题、阅读线程(并发现我的代码中的一些其他问题)之后,我真的很想把这件事做好。因此,如果:
有人可以向我展示如何通过来自另一个非 QThread 控制的线程的另一个 3rd 方库(或其他任何东西)的回调函数调用 QObject 的成员函数的通用方式。
有人可以向我解释为什么选项 1 有效,如果我根本不创建 GUI,即做所有相同的工作,只是没有 QGraphcisScene 对其进行可视化(并且项目
app
是 aQCoreApplication
而不是 aQApplication
并且所有与图形相关的工作都#define
完成了) .
任何,我的意思是绝对任何,我能抓住的稻草真的很感激。
更新
根据接受的答案,我更改了代码以处理来自其他线程的回调:我在void doSomeWorkWithData()
函数的开头引入了线程检查:
void doSomeWorkWithData(A a)
{
if( QThread::currentThread() != this->thread() )
{
QMetaObject::invokeMethod( this,"doSomeWorkWithData"
,Qt::QueuedConnection
,Q_ARG(A, a) );
return;
}
/* The actual work this function does would be below here... */
};
一些相关的想法:
QMutexLocker
我正在考虑在语句之前引入 aif
,但决定反对它:可能并行使用的函数的唯一部分(语句return;
中的任何内容if
) - 据我所知 - 线程安全。手动将连接类型设置为
Qt::QueuedConnection
:从技术上讲,如果我正确理解了文档,Qt 应该做正确的事情,而默认的Qt::AutoConnection
, 应该最终变成Qt::QueuedConnection
. 但是因为当达到那个声明时总是会出现这种情况,所以我决定明确地放在那里以提醒自己为什么会出现这种情况。将排队代码直接放在函数中,而不是将其隐藏在临时函数中:我可以选择将调用
invokeMethod
放在另一个临时函数中,比如queueDoSomeWorkWithData()', which would be called by the callback in the listener and then uses
invokeMethodwith an
Qt::AutoConnection' ondoSomeWorkWithData()
。我决定反对这个,因为我似乎无法通过模板自动编码这个临时函数(模板和元系统是原始问题的一部分),所以我的代码的“用户”(即实现的人doSomeWorkWithData(XYZ xyz)
)会还必须手动键入临时函数(这就是正确解析模板类型名称的方式)。在我看来,在实际函数中包含检查可以安全地键入额外的函数头,使MyClass
界面更简洁,更好地提醒读者doSomeWorkWithData()
可能存在潜伏在黑暗中的线程问题。