跨线程的信号与槽

跨线程的信号与槽
接着上面讨论的,我们如何应用驻足在其他线程里的QObject方法呢?Qt提供了一种非常友好而且干净的解决方案:向事件队列post一个事件,事件的处理将以调用我们所感兴趣的方法为主(当然这需要线程有一个正在运行的事件循环)。而触发机制的实现是由moc提供的内省方法实现的(译者注:有关内省的讨论请参见我的另一篇文章Qt的内省机制剖析):因此,只有信号、槽以及被标记成Q_INVOKABLE的方法才能够被其它线程所触发调用。

静态方法QMetaObject::invokeMethod() 为我们做了如下工作:

[cpp] view plaincopy
QMetaObject::invokeMethod(object, “methodName”,
Qt::QueuedConnection,
Q_ARG(type1, arg1),
Q_ARG(type2, arg2));

请注意,因为上面所示的参数需要被在构建事件时进行硬拷贝,参数的自定义型别所对应的类需要提供一个共有的构造函数、析构函数以及拷贝构造函数。而且必须使用注册Qt型别系统所提供的qRegisterMetaType() 方法来注册这一自定义型别。

跨线程的信号槽的工作方式相类似。当我们把信号连接到一个槽的时候,QObject::connect的第五个可选输入参数用来特化这一连接类型:

direct connection 是指:发起信号的线程会直接触发其所连接的槽;
queued connection 是指:一个事件被派发到接收者所在的线程中,在这里,事件循环会之后的某一时间将该事件拾起并引起槽的调用;
blocking queued connection 与queued connection的区别在于,发送者的线程会被阻塞,直至接收者所在线程的事件循环处理发送者发送(入栈)的事件,当连接信号的槽被触发后,阻塞被解除;
automatic connection (缺省默认参数) 是指: 如果接收者所依附的线程和当前线程是同一个线程,direct connection会被使用。否则使用queued connection。
请注意,在上述四种连接方式当中,发送对象驻足于哪一个线程并不重要!对于automatic connection,Qt会检查触发信号的线程,并且与接收者所驻足的线程相比较从而决定到底使用哪一种连接类型。特别要指出的是:当前的Qt文档的声明(4.7.1) 是错误的:

如果发射者和接受者在同一线程,其行为与Direct Connection相同;,如果发射者和接受者不在同一线程,其行为Queued Connection相同

因为,发送者对象的线程依附性在这里无关紧要。举例子说明

[cpp] view plaincopy
class Thread : public QThread
{
Q_OBJECT
signals:
void aSignal();
protected:
void run() {
emit aSignal();
}
};
/* … */
Thread thread;
Object obj;
QObject::connect(&thread, SIGNAL(aSignal()), &obj, SLOT(aSlot()));
thread.start();

如上述代码,信号aSignal() 将在一个新的线程里被发射(由线程对象所代表);因为它并不是Object 对象驻足的线程,所以尽管Thread对象thread与Object对象obj在同一个线程,但仍然是queued connection被使用。

(译者注:这里作者分析的很透彻,希望读者仔细揣摩Qt文档的这个错误。 也就是说 发送者对象本身在哪一个线程对与信号槽连接类型不起任何作用,起到决定作用的是接收者对象所驻足的线程以及发射信号(该信号与接受者连接)的线程是不是在同一个线程,本例中aSignal()在新的线程中被发射,所以采用queued connection)。

另外一个常见的错误如下:

[c-sharp] view plaincopy
class Thread : public QThread
{
Q_OBJECT
slots:
void aSlot() {
/* … /
}
protected:
void run() {
/
/
}
};
/
… */
Thread thread;
Object obj;
QObject::connect(&obj, SIGNAL(aSignal()), &thread, SLOT(aSlot()));
thread.start();
obj.emitSignal();

当“obj”发射了一个aSignal()信号是,哪种连接将被使用呢?你也许已经猜到了:direct connection。这是因为Thread对象实在发射该信号的线程中生存。在aSlot()槽里,我们可能接着去访问线程里的一些成员变量,然而这些成员变量可能同时正在被run()方法访问:这可是导致完美灾难的秘诀。可能你经常在论坛、博客里面找到的解决方案是在线程的构造函数里加一个moveToThread(this)方法。

class Thread : public QThread {

Q_OBJECT

public:

Thread() {

moveToThread(this); // 错误

}

/* … */

};

(译注:moveToThread(this))

这样做确实可以工作(因为现在线程对象的依附性已经发生了改变),但这是一个非常不好的设计。这里的错误在于我们正在误解线程对象的目的(QThread子类):QThread对象们不是线程;他们是围绕在新产生的线程周围用于控制管理新线程的对象,因此,它们应该用在另一个线程(往往在它们所驻足的那一个线程)

一个比较好而且能够得到相同结果的做法是将“工作”部分从“控制”部分剥离出来,也就是说,写一个QObject子类并使用QObject::moveToThread()方法来改变它的线程依附性:

[cpp] view plaincopy
class Worker : public QObject
{
Q_OBJECT
public slots:
void doWork() {
/* … /
}
};
/
… */
QThread thread;
Worker worker;
connect(obj, SIGNAL(workReady()), &worker, SLOT(doWork()));
worker.moveToThread(&thread);
thread.start();

参考链接

在Qt中,信号机制不仅支持对象之间的通信,还可以用于跨线程通信,实现线程间的安全数据交换和同步操作。跨线程信号机制是Qt多线程编程的核心特性之一,能够确保线程安全并简化并发编程的复杂性。 在多线程环境中使用信号时,必须确保连接方式符合线程安全的要求。Qt提供了多种连接类型,其中 `Qt::AutoConnection`(默认)、`Qt::DirectConnection` 和 `Qt::QueuedConnection` 是最常见的类型。`Qt::AutoConnection` 会根据发送者和接收者的线程上下文自动选择是使用直接连接还是队列连接。如果发送者和接收者位于同一个线程,则使用 `Qt::DirectConnection`,即信号会立即在发送者的线程中调用函数;如果位于不同的线程,则使用 `Qt::QueuedConnection`,即信号会被放入接收者线程的事件队列中,并在该线程的事件循环中执行函数。这种方式确保了线程安全[^1]。 为了使用跨线程信号机制,通常需要结合 `QThread` 或 `QtConcurrent` 等多线程工具。例如,可以将一个工作对象移动到子线程中,并通过信号其进行通信。以下是一个简单的示例: ```cpp class Worker : public QObject { Q_OBJECT public slots: void doWork() { // 执行耗时操作 emit resultReady("Finished"); } signals: void resultReady(const QString& result); }; class Controller : public QObject { Q_OBJECT public: Controller() { QThread* thread = new QThread(this); Worker* worker = new Worker(); worker->moveToThread(thread); connect(thread, &QThread::started, worker, &Worker::doWork); connect(worker, &Worker::resultReady, this, &Controller::handleResult); connect(worker, &Worker::resultReady, thread, &QThread::quit); connect(worker, &Worker::resultReady, worker, &Worker::deleteLater); connect(thread, &QThread::finished, thread, &QThread::deleteLater); thread->start(); } private slots: void handleResult(const QString& result) { qDebug() << "Result:" << result; } }; ``` 在上述代码中,`Worker` 对象被移动到一个新的线程中执行耗时任务,并通过信号 `resultReady` 将结果返回给主线程的 `Controller` 对象。由于使用了 `Qt::QueuedConnection`,信号会在主线程的事件循环中被处理,从而保证线程安全[^2]。 此外,在跨线程通信中需要注意以下几点: - 所有涉及 GUI 的操作必须在主线程中完成,不能在子线程中直接操作 UI。 - 如果需要传递自定义类型的数据,必须使用 `qRegisterMetaType` 注册该类型,以便信号机制能够正确处理。 - 避免在多个线程中同时访问共享资源,否则需要使用互斥锁(`QMutex`)或读写锁(`QReadWriteLock`)等同步机制来保护数据[^3]。 通过合理使用信号机制,可以有效地实现线程间的通信同步,同时避免数据竞争和死锁问题,提高程序的稳定性和性能。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值