7

(伪)代码

这是我遇到问题的概念的不可编译代码草图:

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 ( 12345 ) 中的线程,在我看来

  • 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。

我需要什么帮助

在花了大约一周的时间试图解决这个问题、阅读线程(并发现我的代码中的一些其他问题)之后,我真的很想把这件事做好。因此,如果:

  1. 有人可以向我展示如何通过来自另一个非 QThread 控制的线程的另一个 3rd 方库(或其他任何东西)的回调函数调用 QObject 的成员函数的通用方式。

  2. 有人可以向我解释为什么选项 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我正在考虑在语句之前引入 a if,但决定反对它:可能并行使用的函数的唯一部分(语句return;中的任何内容if) - 据我所知 - 线程安全。

  • 手动将连接类型设置为Qt::QueuedConnection从技术上讲,如果我正确理解了文档,Qt 应该做正确的事情,而默认的Qt::AutoConnection, 应该最终变成Qt::QueuedConnection. 但是因为当达到那个声明时总是会出现这种情况,所以我决定明确地放在那里以提醒自己为什么会出现这种情况。

  • 将排队代码直接放在函数中,而不是将其隐藏在临时函数中:我可以选择将调用invokeMethod放在另一个临时函数中,比如queueDoSomeWorkWithData()', which would be called by the callback in the listener and then usesinvokeMethod with anQt::AutoConnection' on doSomeWorkWithData()。我决定反对这个,因为我似乎无法通过模板自动编码这个临时函数(模板和元系统是原始问题的一部分),所以我的代码的“用户”(即实现的人doSomeWorkWithData(XYZ xyz))会还必须手动键入临时函数(这就是正确解析模板类型名称的方式)。在我看来,在实际函数中包含检查可以安全地键入额外的函数头,使MyClass界面更简洁,更好地提醒读者doSomeWorkWithData()可能存在潜伏在黑暗中的线程问题。

4

1 回答 1

4

如果您确定单个函数将仅执行线程安全操作,则可以从另一个线程调用 QObject 子类上的公共函数。

Qt 的一个优点是它可以像处理 QThreads 一样处理外来线程。threadSafeDoSomeWorkWithData因此,一种选择是为每个函数创建一个除了非线程安全的函数doSomeWorkWithData之外什么都不做的函数。QMetaMethod::invoke

public:
  void threadSafeDoSomeWorkWithData(A a) { 
    QMetaMethod::invoke("doSomeWorkWithData", Q_ARG(A,a)); 
  } 
  Q_INVOKABLE void doSomeWorkWithData(A a);

或者,Sergey Tachenov 在他的回答中提出了一种有趣的方式来做或多或少相同的事情。他将我建议的两种功能合二为一。

void Obj2::ping() {
    if (QThread::currentThread() != this->thread()) {
        // not sure how efficient it is
        QMetaObject::invoke(this, "ping", Qt::QueuedConnection);
        return;
    }
    // thread unsafe code goes here
}

至于为什么您在不创建 GUI 时会看到正常行为?除了操作 GUI 对象之外,您可能没有做任何其他不安全的事情。或者,也许它们是您的线程安全问题显而易见的唯一地方。

于 2013-03-06T06:37:53.050 回答