之所以要“拨乱反正”,是因为很多教科书上的说法,还有网页上的说法,都是错误的。
我没有看过qt源码,看过一些书籍,做过一些实验,说下我的理解。如有谬误,还请讨论。
首先来看看教科书上原版错误或者说混乱的说法:
你可以尝试用搜索引擎输入“qt connect第五个参数”,那么将得到下面主流的说法。但是这些人都是人云亦云,瞎抄一通,根本没有深入理解字段的说法。
第五个参数代表槽函数在哪个线程中执行 :
1)自动连接(AutoConnection),默认的连接方式,如果信号与槽,也就是发送者与接受者在同一线程,等同于直接连接;如果发送者与接受者处在不同线程,等同于队列连接。
2)直接连接(DirectConnection),当信号发射时,槽函数立即直接调用。无论槽函数所属对象在哪个线程,槽函数总在发送者所在线程执行,即槽函数和信号发送者在同一线程
3)队列连接(QueuedConnection),当控制权回到接受者所在线程的事件循环时,槽函数被调用。槽函数在接受者所在线程执行,即槽函数与信号接受者在同一线程
4)锁定队列连接(QueuedConnection)
Qt::BlockingQueuedConnection:槽函数的调用时机与Qt::QueuedConnection一致,不过发送完信号后发送者所在线程会阻塞,直到槽函数运行完。接收者和发送者绝对不能在一个线程,否则程序会死锁。在多线程间需要同步的场合可能需要这个。
5)单一连接(QueuedConnection)
Qt::UniqueConnection:这个flag可以通过按位或(|)与以上四个结合在一起使用。当这个flag设置时,当某个信号和槽已经连接时,再进行重复的连接就会失败。也就是避免了重复连接
拨乱反正点:“发送者”概念错误。上面红色加粗的字段,都是说法混淆、或者错误的说法。
“发送者”这个概念,存在相当大的错误或概念混淆。
何为发送者?从字面意思和文字意思来理解,是指(1)信号所在对象,还是指(2)“信号所在对象”所在的线程,还是指(3)当前调用emit触发信号的线程。
所有那些人云亦云的家伙,都没用实实在在搞明白,发送者是指(1)(2)(3)中的哪一个。有些人认为是(1),更多人认为是(2),但是真正严格的答案,我通过实验得出,却是(3)。
举个例子,有两个线程T1、T2,对象A在T1中,对象A有一个信号sig1,那么当在T2里面调用A的sig1时,该怎么解释上面说的,谁是信号的“发送者”?
具体用代码来表示。
/*我故意设计了这样一个例子来验证问题的严重性*/
class Thread :public QThread
{
public:
Thread();
Thread(Test *outObj) { m_outObj = outObj; };
virtual ~Thread();
protected:
void run() {
qDebug("new thread run in %p", currentThread());
emit m_outObj->sig_test();
}
private:
Test * m_outObj;
};
class Test :
public QObject
{
Q_OBJECT;
public:
Test();
virtual ~Test();
signals:
void sig_test();
public slots:
void slot_test() { qDebug("Test slots in %p\n", QThread::currentThread()); }
};
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
qDebug("main in %p\n", QThread::currentThread());
Test t;
QObject::connect(&t, &Test::sig_test, &t, &Test::slot_test, Qt::DirectConnection);
Thread *thread = new Thread(&t);
thread->start();
return a.exec();
}
上面代码只是示例。
上面代码就描述了前文的情况。我有两个线程T1(主线程)、T2(子线程thread),对象Test t 是在主线程T1中,当我们在T2的run里面发送信号 t.sig_test()时,谁是信号的发送者?注意为了抓出发送者,我用的是直连方式,因为在直连方式下,“槽函数的执行线程"与“发送者”在一个线程,这样通过槽函数在哪个线程,我们就知道谁是“发送者”。
看看输出:
main in 0x9036d8
new thread run in 0x909858
Test slots in 0x909858
没错!槽函数是在子线程中,那么说明“发送者”也是子线程! 因为我们用的是直连方式。
关于发送者是谁,第(1)中说法 “信号所在的对象”,是错误地。对象只是一个内存对象,它本身无法执行。这种说法一开始就排除。第(2)中说法“信号所在对象”所在的线程,也是错误。我们知道这里“信号所在对象”是 t ,而t 是在主线程定义的,那么按这个说法,发送者应该是主线程才对,但事实却不是,实践证明是子线程中发送的。
第(3)中说法,调用emit触发信号的线程。没错,我们是在run子线程中调用的emit,所以发送者就是子线程本身。事实证明也是如此。
如果把上面的connect的第五个参数修改为队列连接,槽函数在哪里执行,答案是什么?我们先说答案。
QObject::connect(&t, &Test::sig_test, &t, &Test::slot_test, Qt::QueuedConnection);
main in 0x8042a0
new thread run in 0x80a768
Test slots in 0x8042a0
槽函数在主线程中执行。这点是没有什么好怀疑的。因为队列模式下,槽函数是在接受者对象所在的线程中执行,与发送者无关。而接受者 t 就是在主线程中,那么槽函数就在主线程执行,这是没有疑问的。
我们一定要有一个概念,槽函数在什么地方执行,是动态决定的,不是写connect时决定的。如果是写connect时就决定,那么上面的写法,总是给人一种都是在主线程中执行的错觉。
三个核心要素,来决定槽函数在哪个线程调用。
(1) 调用emit 发送信号的线程。
(2)接受者对象所在的线程。
(3)connect的第五个参数。
最容易忽略,而且最容易出错的是第一个要素。
那么为了表达准确,我们现在用准确的语句,来重写上面的准则。
第五个参数代表槽函数在哪个线程中执行 :
1)自动连接(AutoConnection),默认的连接方式,如果信号与槽,也就是“发送信号的线程”与“接受者所在的线程“是同一线程,等同于直接连接;如果“发送信号的线程”与“接受者所在的线程”不是一个线程,等同于队列连接。
2)直接连接(DirectConnection),当信号发射时,槽函数立即直接调用。无论槽函数所属对象在哪个线程,槽函数总在“发送信号的线程”中执行,即槽函数和“信号发送线程”在同一线程
3)队列连接(QueuedConnection),当控制权回到接受者所在线程的事件循环时,槽函数被调用。槽函数在接受者所在线程执行,即槽函数与"信号接受者所在线程"在同一线程
4)锁定队列连接(QueuedConnection)
Qt::BlockingQueuedConnection:槽函数的调用时机与Qt::QueuedConnection一致,不过发送完信号后“发送信号的线程”会阻塞,直到槽函数运行完。“接收者所在线程”和“发送信号的线程”绝对不能在一个线程,否则程序会死锁。在多线程间需要同步的场合可能需要这个。
5)单一连接(QueuedConnection)
Qt::UniqueConnection:这个flag可以通过按位或(|)与以上四个结合在一起使用。当这个flag设置时,当某个信号和槽已经连接时,再进行重复的连接就会失败。也就是避免了重复连接。我们再次重申:“发送信号的线程”就是指代码中调用 emit 信号时的执行线程;而绝不是“信号所在对象“所属的线程。
“信号所在对象”所属的线程,是创建该对象的线程。“信号所在对象”所属的线程,不是一层不变的,可以通过moveToThread改变。
上文可谓字字珠玑。许多糊涂蛋把“发送信号的线程” 搞错为“发送者所在线程”,后者干脆模糊不清用“发送者”代替。这些是大错特错的。简直是以讹传讹,基本上目前我看到的所有已有版本都是错误的。
关于:moveToThread
该函数会改变对象所在的线程,对象所在的线程使用Qobject::thread()函数查看,当前线程使用currentThread()查看。二者容易混淆,这是两个不同的概念。该函数改变的是前者。
还是上面的例子,我们把t移动到子线程中去,还是采用队列连接方式。由于队列连接时,槽函数在接受者所在线程执行,与发送者线程无关,所以这时槽函数就会在子线程执行。
代码如下:
class Thread :public QThread
{
public:
Thread();
Thread(Test *outObj) { m_outObj = outObj; };
virtual ~Thread();
protected:
void run() {
qDebug("new thread run in %p", currentThread());
emit m_outObj->sig_test();
int i = 0;
while (i++ < 10) /*延时退出,否则槽函数没有机会执行*/
{
QCoreApplication::processEvents();
}
}
private:
Test * m_outObj;
};
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
qDebug("main in %p\n", QThread::currentThread());
Test *t = new Test;
QObject::connect(t, &Test::sig_test, t, &Test::slot_test, Qt::QueuedConnection);
qDebug("t now in %p\n", t->thread());
Thread *thread = new Thread(t);
thread->start();
t->moveToThread(thread);
qDebug("t now in %p\n", t->thread());
return a.exec();
}
/*Test代码没有修改,不贴出*/
运行结果毫无意外地,t moveToThread后,其所在的线程发生了变化。
main in 0x2241b0
t now in 0x2241b0
t now in 0x228d10
new thread run in 0x228d10
Test slots in 0x228d10
最终槽函数是在子线程中执行的。
注意我们在子线程中加入了延时处理,见代码中的注释。如果不加的话,那么提交emit 信号后,子线程继续退出。后面就没有机会再去执行信号的槽函数了。不信的话,你去掉上面的延时函数,槽函数就无法正常打印出了。
原因是因为使用的队列连接方式,需要回到子线程的事件循环中后,才会执行槽函数。但是子线程提交信号后直接退出,再也不会回到它的事件循环中,就没有机会执行槽函数拉。
这是特别需要注意的一点。直接连接时,不需要等到进入事件循环中才能执行槽函数,而且直接就立即调用槽函数啦。而队列连接,会有一个延时,信号在事件队列中排队,只有线程进入到事件消息处理中,才能去执行槽函数。