在带有 GCD 的 ObjC 中,有一种方法可以在任何旋转事件循环的线程中执行 lambda。例如:
dispatch_sync(dispatch_get_main_queue(), ^{ /* do sth */ });
或者:
dispatch_async(dispatch_get_main_queue(), ^{ /* do sth */ });
它在主线程的队列中执行一些东西(相当于C++中的 []{ /* do sth */ }
),无论是阻塞的还是异步的。
我怎样才能在 Qt 中做同样的事情?
根据我的阅读,我想解决方案是以某种方式向主线程的某个对象发送信号。但是什么对象?只是 QApplication::instance()
? (那是当时唯一存在于主线程中的对象。)什么信号?
从目前的答案和我目前的研究来看,我确实需要一些虚拟对象来坐在主线程中,并带有一些插槽,它只是等待一些代码执行。
所以,我决定 QApplication
来添加它。我当前的代码不起作用(但也许您可以提供帮助):
#include <QApplication>
#include <QThread>
#include <QMetaMethod>
#include <functional>
#include <assert.h>
class App : public QApplication
{
Q_OBJECT
public:
App();
signals:
public slots:
void genericExec(std::function<void(void)> func) {
func();
}
private:
// cache this
QMetaMethod genericExec_method;
public:
void invokeGenericExec(std::function<void(void)> func, Qt::ConnectionType connType) {
if(!genericExec_method) {
QByteArray normalizedSignature = QMetaObject::normalizedSignature("genericExec(std::function<void(void)>)");
int methodIndex = this->metaObject()->indexOfSlot(normalizedSignature);
assert(methodIndex >= 0);
genericExec_method = this->metaObject()->method(methodIndex);
}
genericExec_method.invoke(this, connType, Q_ARG(std::function<void(void)>, func));
}
};
static inline
void execInMainThread_sync(std::function<void(void)> func) {
if(qApp->thread() == QThread::currentThread())
func();
else {
((App*) qApp)->invokeGenericExec(func, Qt::BlockingQueuedConnection);
}
}
static inline
void execInMainThread_async(std::function<void(void)> func) {
((App*) qApp)->invokeGenericExec(func, Qt::QueuedConnection);
}
原文由 Albert 发布,翻译遵循 CC BY-SA 4.0 许可协议
这当然是可能的。任何解决方案都将集中于传递一个将函子包装到驻留在所需线程中的消费者对象的事件。我们将此操作称为元调用发布。细节可以通过多种方式执行。
Qt 5.10 及更高版本 TL;DR
TL;DR 用于函子
TL;方法/槽的 DR
TL;DR:单次计时器呢?
上述所有方法都适用于没有事件循环的线程。由于 QTBUG-66458 ,方便地使用
QTimer::singleShot
需要源线程中的事件循环。然后postToObject
变得非常简单,你可以直接使用QTimer::singleShot
,虽然这是一个尴尬的名字,隐藏了那些不熟悉这个成语的人的意图。即使您不需要类型检查,通过命名为更好地指示意图的函数的间接也是有意义的:通用代码
让我们根据以下通用代码来定义我们的问题。最简单的解决方案是将事件发布到应用程序对象(如果目标线程是主线程),或者发布到任何其他给定线程的事件分派器。由于事件调度器仅在
QThread::run
被输入后才存在,我们通过从needsRunningThread
返回 true 来指示线程运行的要求。最简单形式的元调用发布函数需要仿函数调用使用者为给定线程提供对象,并实例化仿函数调用事件。事件的实现还摆在我们面前,是各种实现的本质区别。
第二个重载对函子采用右值引用,可能会在函子上保存复制操作。如果延续包含复制成本高昂的数据,这将很有帮助。
出于演示目的,工作线程首先向主线程发布元调用,然后按照
QThread::run()
启动事件循环以侦听来自其他线程的可能的元调用。互斥锁用于允许线程用户以简单的方式等待线程启动,如果消费者的实现需要的话。对于上面给出的默认事件使用者,这种等待是必要的。最后,我们启动上面的工作线程,它向主(应用程序)线程发布一个元调用,应用程序线程向工作线程发布一个元调用。
在所有实现中,输出将大致如下所示。函子跨线程:在主线程中创建的函子在工作线程中执行,反之亦然。
使用临时对象作为信号源的 Qt 5 解决方案
Qt 5 最简单的方法是使用临时的
QObject
作为信号源,并将仿函数连接到它的destroyed(QObject*)
信号。当postMetaCall
返回时,signalSource
被破坏,发出它的destroyed
信号,并将元调用发送到代理对象。这可能是 C++11 风格中最简洁直接的实现。
signalSource
对象以 C++11 RAII 方式用于其破坏的副作用。短语“副作用”在 C++11 的语义中具有含义,不应被解释为“不可靠”或“不受欢迎的”——它绝不是。QObject
与我们的合同是在其析构函数执行期间的某个时间发出destroyed
。我们非常欢迎使用这一事实。如果我们只打算发布到主线程,代码变得几乎是微不足道的:
使用 QEvent 析构函数的 Qt 4⁄5 解决方案
同样的方法可以直接应用于
QEvent
。事件的虚拟析构函数可以调用函子。这些事件在被消费者对象的线程的事件分派器传递后立即被删除,因此它们总是在正确的线程中执行。这在 Qt 4⁄5 中不会改变。仅发布到主线程,事情变得更加简单:
使用私有 QMetaCallEvent 的 Qt 5 解决方案
仿函数可以包装在
QMetaCallEvent
的 Qt 5 插槽对象有效负载中。仿函数将由QObject::event
调用,因此可以发布到目标线程中的任何对象。此解决方案使用 Qt 5 的私有实现细节。使用自定义事件和消费者的 Qt 4⁄5 解决方案
我们重新实现对象的
event()
方法,并让它调用函子。这要求在函子发布到的每个线程中都有一个显式的事件使用者对象。对象在其线程完成时被清理,或者,对于主线程,当应用程序实例被破坏时。它适用于 Qt 4 和 Qt 5。右值引用的使用避免了临时函子的复制。