本文档将介绍如下内容:
- QT 信号槽机制
- 多线程下qt注意点
- 一些qt心得知识点
1)一个带入点问题
这个问题,重在搞明白QT的信号、槽函数在何时、何地、由谁发出、由谁执行。
不要小看这个例子,笔者见过一些“用QT工作过五六年”的人士,被问到该问题时还是“王顾左右而言他”,不知道该怎么回答。可以想象,这些人只能算处于使用 QT的初级阶段,连核心问题的门都还没有摸到。
在回答这个问题前,我们必须要介绍一些基础知识。
2)对象属于哪个线程
给出一个代码片段,借此说明问题:
class MyThread:public QThread { MyThread(){p1 = new A()} //p1对象在旧线程 void run(){p2 = new A()}//p2对象在新线程 } void mian() { MyThread thread1; //thread1对象在旧线程中 thread1.start(); }
QT 多线程下只有QThread::run() 函数是在新线程中。
run中new的对象,是在新线程中。
除此以外,构造函数中new的对象,线程对象本身,还是在旧线程中。
由于MyThread的构造函数还是在主线程中调用的,所以p1是在主线程中。
这几点非常关键。
3)教科书中令人疑惑的QT CONNECT第五个参数
第五个参数代表槽函数在哪个线程中执行 :
1)自动连接(AutoConnection),默认的连接方式,如果信号与槽,也就是发送者与接受者在同一线程,等同于直接连接;如果发送者与接受者处在不同线程,等同于队列连接。
2)直接连接(DirectConnection),当信号发射时,槽函数立即直接调用。无论槽函数所属对象在哪个线程,槽函数总在发送者所在线程执行,即槽函数和信号发送者在同一线程
3)队列连接(QueuedConnection),当控制权回到接受者所在线程的事件循环时,槽函数被调用。槽函数在接受者所在线程执行,即槽函数与信号接受者在同一线程
4)锁定队列连接(QueuedConnection)
Qt::BlockingQueuedConnection:槽函数的调用时机与Qt::QueuedConnection一致,不过发送完信号后发送者所在线程会阻塞,直到槽函数运行完。接收者和发送者绝对不能在一个线程,否则程序会死锁。在多线程间需要同步的场合可能需要这个。
5)单一连接(QueuedConnection)
Qt::UniqueConnection:这个flag可以通过按位或(|)与以上四个结合在一起使用。当这个flag设置时,当某个信号和槽已经连接时,再进行重复的连接就会失败。也就是避免了重复连接
注意:此处教科书说法“可能”有歧义错误。后续我们会纠正这里的错误。
如果你第一次看到这样的说法,有没有对“发送者”这个概念感到疑惑。或者说,仅仅用这里的描述,你可以回答最开篇我们的提问吗?
4)回到例子
在开篇的例子中,我们加入一个新的限定,信号和槽采用直连接方式。
1)pa 属于主线程
2)采用直接连接方式
信号发送者是谁,是主线程还是子线程,还是其它什么“东西” ?
注:不清楚这个概念,无法清晰回答左边三个问题,就还不算完全理解了多线程下信号槽机制。
答案:信号的发送者是线程,不是其它“东西”,而且是子线程。由于是直连方式,槽函数是在子线程中调用。
何时调用?类似函数指针的方式,在emit提交的时候,直接类似调用“函数指针”的方式立刻在子线程中执行。
5)纠正教科书的说法
第五个参数代表槽函数在哪个线程中执行 :
1)自动连接(AutoConnection),默认的连接方式,如果信号与槽,也就是“发送信号的线程”与“接受者所在的线程”是同一线程,等同于直接连接;如果“发送信号的线程”与“接受者所在的线程”不是一个线程,等同于队列连接。
2)直接连接(DirectConnection),当信号发射时,槽函数立即直接调用。无论槽函数所属对象在哪个线程,槽函数总在“发送信号的线程”中执行,即槽函数和“信号发送线程”在同一线程
3)队列连接(QueuedConnection),当控制权回到接受者所在线程的事件循环时,槽函数被调用。槽函数在接受者所在线程执行,即槽函数与"信号接受者所在线程"在同一线程
4)锁定队列连接(QueuedConnection)
Qt::BlockingQueuedConnection:槽函数的调用时机与Qt::QueuedConnection一致,不过发送完信号后“发送信号的线程”会阻塞,直到槽函数运行完。“接收者所在线程”和“发送信号的线程”绝对不能在一个线程,否则程序会死锁。在多线程间需要同步的场合可能需要这个。
5)单一连接(QueuedConnection)
Qt::UniqueConnection:这个flag可以通过按位或(|)与以上四个结合在一起使用。当这个flag设置时,当某个信号和槽已经连接时,再进行重复的连接就会失败。也就是避免了重复连接。
注意点:
“发送信号的线程”就是指代码中调用 emit 信号时的执行线程;而不是“信号所在对象“所属的线程。
三个要素,来决定槽函数在哪个线程调用。
- 调用emit 发送信号的线程。
- 接受者对象所在的线程。
- connect的第五个参数。
注:槽函数在何处执行是动态决定的,而不是在写connect函数时(编译时)决定的。
6)槽函数何时执行
每个线程均有自己的消息事件循环队列
直连方式,是所谓立即执行,就是函数直接调用。
队列方式,不会立即执行,分单/多线程情况:
单线程(发送、接收同一线程):消息放入消息队列。线程进入消息队列时,依次执行队列上消息对应槽函数。
跨线程(发送、接收不同线程):消息放入接收者线程队列。接收者线程运行时(可能阻塞、睡眠、或退出),进入它的消息队列后,依次执行队列上的消息。这也意味着,同一个槽函数不会重入,不用考虑重入互斥访问,因为都在队列上排队等待依次执行(注意不要乱用postEvents函数,后续我们有机会单独讲一讲该问题)。
7)一个错误示范
class Thread :public QThread{ public: Thread(); Thread(Test *outObj) { m_outObj = outObj; }; virtual ~Thread(); protected: void run() { emit m_outObj->sig_test(); } private: Test * m_outObj; }; int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); Test *t = new Test; QObject::connect(t, &Test::sig_test, t, &Test::slot_test, Qt::QueuedConnection); Thread *thread = new Thread(t); t->moveToThread(thread); thread->start(); return a.exec(); }
如果你完全理解了上述要素,这个例子会有什么问题吗?
分析如下:
- 上例是队列连接,槽函数会在接受者所在线程(who),等到该线程回到事件循环后(when)执行槽函数。
- T 调用moveToThread后,t作为接受者,已经被移到到子线程中去。
- 子线程执行start后,run中调用emit信号触发后,子线程不会立刻执行槽函数;而是等到子线程回到事件循环后,才会执行槽函数。但是子线程此时会直接退出;不会再有机会回到子线程的事件循环。
最终导致槽函数没有机会再去执行。
8)QT多线程编程注意点
1 QT主线程不能阻塞。因为UI在主线程中,阻塞则界面卡死。使用while()QCoreApplication::processEvents(); qapp->processEvents()替换睡眠操作。[重要]
2 非主线程不能操作UI控件,否则QT崩溃。这也是要分离界面与逻辑的重要原因。[重要]
3 父子QObject对象,必须在同一个线程;不同线程的对象,不能是父子关系。
否则会报错,或者产生未知情况。[重要]
4 官方优先推荐使用moveToThread方式,其次使用继承QThread方式。理解后都一样。
5 注意槽函数是在线程中执行。如果执行线程睡眠、阻塞,槽函数没有机会执行。如果你的槽函数要快速响应,不要让它在可能阻塞或睡眠的线程中。[重要]
6 要注意volidate修饰共享变量、要注意加锁。不同的锁行为会导致线程不同状态,得根据线程业务状态去考虑用什么锁。[重要]
7 活用慎用processEvents
线程(包括主线程和其它线程)执行很繁重的计算任务中,为防止消息事件一直无机会处理,则在函数中手动调用processEvents,让消息事件有机会更新。界面不会假死。
另一方面,槽函数中不可调用processEvents,因为这会导致“中断”当前槽函数,进而去执行消息队列中后续的槽函数,可能会引发同一槽函数重入问题,将编程问题复杂化。
8 new Qobject对象哪些需要手动释放?
一个QObject对象,如果有父节点,则可以不手动delete释放,父节点释放时会自动去释放所有子节点;反之没有父节点,须手动调用delete释放。
delete QObject时,会把对象从父节点摘掉;再删除并释放它的所有子节点。一些addChild操作,会主动把对象加入父节点。父子关系不是可有可无的,会涉及到对象内存回收问题,要做到心中有数。
尾语
最近面试过一些号称做过多年QT开发的程序员,有些连QT第5个参数要么没听过,要么听过却没有深入理解原理。可想而知,这些人在平时工作中要么没有深入思考,要么没有深入刨根问底。或许更多的人处于没有机会去触及这些本质问题。因为他们实在太忙了,忙于低水平的原地重复。
写此文,纯粹是为了表达下内心所想。限于本人水平有限,如有错误,还请赐教讨论。