如何将 lambda 函数排队到 Qt 的事件循环中?

Posted

技术标签:

【中文标题】如何将 lambda 函数排队到 Qt 的事件循环中?【英文标题】:How to queue lambda function into Qt's event loop? 【发布时间】:2017-07-22 19:52:35 【问题描述】:

基本上我需要在 Java 中完成的相同操作:

SwingUtilities.invokeLater(()->/* function */);

或者像这样在javascript中:

setTimeout(()=>/* function */, 0);

但是使用 Qt 和 lambda。所以一些伪代码:

Qt::queuePushMagic([]()  /* function */ );

作为一个额外的复杂因素,我需要它在多线程上下文中工作。我实际上想要做的是在正确的线程中自动运行某些方法。然后代码会是什么样子:

SomeClass::threadSafeAsyncMethod() 
    if(this->thread() != QThread::currentThread()) 
        Qt::queuePushMagic([this]()=> this->threadSafeAsyncMethod() );
        return;
    

如何做到这一点?

【问题讨论】:

你试过QTimer吗? 我猜这可能行得通。但是要提一提,而且相当重要的一件事,就是我正试图推入 其他线程的事件循环。 【参考方案1】:

您的问题是How to leverage Qt to make a QObject method thread-safe? 让我们根据您的用例调整那里提供的解决方案。首先,让我们排除安全检查:

bool isSafe(QObject * obj) 
   Q_ASSERT(obj->thread() || qApp && qApp->thread() == QThread::currentThread());
   auto thread = obj->thread() ? obj->thread() : qApp->thread();
   return thread == QThread::currentThread();

您建议的方法采用函子,并让编译器处理在函子中打包参数(如果有的话):

template <typename Fun> void postCall(QObject * obj, Fun && fun) 
   qDebug() << __FUNCTION__;
   struct Event : public QEvent 
      using F = typename std::decay<Fun>::type;
      F fun;
      Event(F && fun) : QEvent(QEvent::None), fun(std::move(fun)) 
      Event(const F & fun) : QEvent(QEvent::None), fun(fun) 
      ~Event()  fun(); 
   ;
   QCoreApplication::postEvent(
            obj->thread() ? obj : qApp, new Event(std::forward<Fun>(fun)));

第二种方法将所有参数的副本显式存储在事件中,并且不使用仿函数:

template <typename Class, typename... Args>
struct CallEvent : public QEvent 
   // See https://***.com/a/7858971/1329652
   // See also https://***.com/a/15338881/1329652
   template <int ...> struct seq ;
   template <int N, int... S> struct gens  using type = typename gens<N-1, N-1, S...>::type; ;
   template <int ...S>        struct gens<0, S...>  using type = seq<S...>; ;
   template <int ...S>        void callFunc(seq<S...>)  (obj->*method)(std::get<S>(args)...); 
   Class * obj;
   void (Class::*method)(Args...);
   std::tuple<typename std::decay<Args>::type...> args;
   CallEvent(Class * obj, void (Class::*method)(Args...), Args&&... args) :
      QEvent(QEvent::None), obj(obj), method(method), args(std::move<Args>(args)...) 
   ~CallEvent()  callFunc(typename gens<sizeof...(Args)>::type()); 
;

template <typename Class, typename... Args> void postCall(Class * obj, void (Class::*method)(Args...), Args&& ...args) 
   qDebug() << __FUNCTION__;
   QCoreApplication::postEvent(
            obj->thread() ? static_cast<QObject*>(obj) : qApp, new CallEvent<Class, Args...>obj, method, std::forward<Args>(args)...);

用法如下:

struct Class : QObject 
   int num;
   QString str;
   void method1(int val) 
      if (!isSafe(this))
         return postCall(this, [=] method1(val); );
      qDebug() << __FUNCTION__;
      num = val;
   
   void method2(const QString &val) 
      if (!isSafe(this))
         return postCall(this, &Class::method2, val);
      qDebug() << __FUNCTION__;
      str = val;
   
;

测试工具:

// https://github.com/KubaO/***n/tree/master/questions/safe-method-40382820
#include <QtCore>

// above code

class Thread : public QThread 
public:
   Thread(QObject * parent = nullptr) : QThread(parent) 
   ~Thread()  quit(); wait(); 
;

void moveToOwnThread(QObject * obj) 
  Q_ASSERT(obj->thread() == QThread::currentThread());
  auto thread = new Threadobj;
  thread->start();
  obj->moveToThread(thread);


int main(int argc, char ** argv) 
   QCoreApplication appargc, argv;
   Class c;
   moveToOwnThread(&c);

   const auto num = 44;
   const auto str = QString::fromLatin1("Foo");
   c.method1(num);
   c.method2(str);
   postCall(&c, [&] c.thread()->quit(); );
   c.thread()->wait();
   Q_ASSERT(c.num == num && c.str == str);

输出:

postCall 
postCall 
postCall 
method1 
method2 

上面的代码编译和使用 Qt 4 或 Qt 5。

另请参阅this question,探索在 Qt 的其他线程上下文中调用函子的各种方法。

【讨论】:

这看起来很棒。我已经在研究其他提案,但又遇到了一个问题——如果线程还没有运行怎么办?这可能是我设计中的问题。我所做的是在QThread::run 的重新实现版本中调用this-&gt;moveToThread(this)。我什至不确定这个问题是否相关,或者我只是做错了。 发布到给定对象的事件会在对象切换线程时跟踪该对象,因此这不是问题。但是this-&gt;moveToThread(this) 是一种反模式。您无需触摸QThread。将您的代码放入 QObject 并将其移至已运行的线程。 所以我不应该维护QThread 子类,然后在它们代表的线程中运行?我认为它是这样工作的。我当然可以单独制作线程,但我认为这只是更多的代码。 一般来说,当在线程中使用QObject 时,你根本不需要子类化QThread,除非让它的析构函数合理(例如~MyThread() quit(); wait(); 。本质上,做你想做的事做,但继承自QObject,而不是QThread。您可以使用safe 的变体强制将方法调用延迟到构造函数的稍后时间,如果您想将一些构造工作推迟到控件返回事件循环。如果对象和线程之间存在 1:1 的关系,则可以让对象保留其线程 - 见上文。 有两种方法:一种是通过阻塞连接,另一种是通过通知信号。一般来说,阻塞 GUI 线程是一个非常糟糕的主意,因此首选通知。我将修改示例以展示如何完成。【参考方案2】:

从 Qt 5.10 开始,您可以这样做:

QMetaObject::invokeMethod(obj, []  );

obj 是一个 QObject,它被分配给你希望你的东西在其中运行的线程。

【讨论】:

并确保它到达队列的末尾(即使当前执行已经在与目标obj 相同的线程上),您可以显式指定Qt::QueuedConnection 参数: QMetaObject::invokeMethod(obj, "doSomething", Qt::QueuedConnection);【参考方案3】:

如果你使用

QMetaObject::invokeMethod(obj, []  );

从事件循环线程中,lambda 被立即执行,但是

QTimer::singleShot(1, [=]() );

在所有其他任务之后运行。你也可以写这个来给循环更多的时间来处理这个时候挂起的事件:

QTimer::singleShot(1, [=]() 
  QApplication::processEvents();  
  // .....
);

【讨论】:

以上是关于如何将 lambda 函数排队到 Qt 的事件循环中?的主要内容,如果未能解决你的问题,请参考以下文章

将使用事件循环的 C++/Qt 线程转换为使用 Dispatchers 的 C#(或 java)线程的可能性

使用nodejs中的AWS Lambda函数上传音频文件

QT中的线程与事件循环理解

如何在 Qt 的主事件循环中使用 std::thread?

Nodejs 事件循环

在 Qt5 中断开 lambda 函数