个人笔记,如有异议,欢迎讨论。
以前用vb或者c#的时候,需要响应哪个事件,就可以添加响应函数来做相应处理。vc++的mfc也是如此。但是它内部是如何实现的,即时暂时不知道,大多数情况下也没有影响做项目。但是在qt中,消息和槽成为了比较通用的概念,很多时候更善于利用它进行参数传递。但事实上,消息和槽机制,离不开事件循环。
概念:所谓事件循环,就是循环执行消息响应,此时一旦消息队列有消息发生,就可以马上执行槽响应函数。
场景——main函数:就如常见的main函数,主窗体打开后,后面有个exec(),这就是事件循环。可以理解为一个死循环,永远在等待消息队列。所以,在窗体中放个控件,才可以有槽函数来处理信号响应。
#include "mainwindow.h" #includeint main(int argc, char *argv[]) { QApplication a(argc, argv); MainWindow w; w.show(); return a.exec();//事件循环 }
如果没有它,主窗体会闪一下就过去了。比如这样:
#include "mainwindow.h" #includeint main(int argc, char *argv[]) { QApplication a(argc, argv); MainWindow w; w.show(); return 0; //return a.exec(); }
像这样main函数以很快的速度创建一个窗体并显示一下,然后又顺理成章地返回0,主进程随着main函数的结束而结束,窗体只是闪一下,什么都干不了。
场景——run函数:写多线程的时候,可以从QThread派生一个线程类,而且要重写run函数。重写run函数的时候,底部要加一个exec()事件循环。
void MyThread::run() { //希望在线程中完成的操作... exec();//事件循环 }
跟main函数一样,这里必须写事件循环,否则run函数执行完前面代码之后会直接结束,线程就结束了。
这里穿插一个概念,所谓线程,不是new了一个线程对象就是线程,这个线程对象其实是在父线程中,跟其它对象一样,new了一个实例而已。它仅仅存在于父线程,它可以作为控制线程的句柄。而真正的线程过程,是run函数启动以后,写在run函数中的代码。所以使用继承QThread并重写run函数的方式实现线程时,一定切记,不是所有函数就一定会在线程中执行,除非它被run函数调用,或者在run当中使用rambda写匿名槽函数。而写匿名槽函数的时候,接收者千万别写this,this指针是指向父线程的线程对象,能作为句柄控制线程,但this隶属于父线程。所以一旦写了this是接收者,这个匿名槽函数会在父线程执行。而子线程中创建对象时,也不要指定parent为this。
所以,在run函数中写exec,它会阻止run函数结束,让子线程始终等待消息队列的任务,从而实现利用信号槽进行线程通信。
场景——子线程对象上面说过线程的实现,离不开父线程的线程对象,它仅仅是子线程的操作句柄。如果一定要让一个槽函数运行于子线程中,可能还少不了要写个对象再使用movetothread让它进入线程。所以,个人认为,写线程的时候,更好的方法是不要继承QThread并重写run函数。而是把要执行的逻辑写成一个类,实例化以后movetothread,可以确保它一定是在子线程中运行。
MyObject *obj = new MyObject; QThread *thd = new QThread; obj->setParent(NULL); obj->moveToThread(thd); thd->start();
如果是不太繁忙的工作,可能不需要考虑下面的问题。但我写了一个模拟生产者的逻辑,使用了while循环,这中间需要写sleep来让出cpu资源。比如下面代码中有个变量m_bStop作为标记用来停止工作流程,但是这个变量什么时候生效?就要sleep之后,它才有机会被外部线程修改并生效。
while (!m_bStop) { //Do something //这是必须的,否则当前线程无休止循环,根本没有机会让m_bStop变量被外部更改。 //可以用qDebug在sleep前后分别输出一下m_bStop的值,立见分晓。 QThread::msleep(500); }
有个m_bStop变量用于控制退出循环从而结束工作流程。如果把m_bStop声明成public volatile,这样从线程外面可以控制线程中的流程什么时候结束。
但如果有些参数需要调整,其实也可以用这种方式。但如果想使用信号和槽,上面的写法行不通的。虽然sleep出让了cpu资源,它只是阻塞当前线程,并不会让当前线程“休息”来等待响应信号。所以这里还要用事件循环。于是就有了下面的写法。
while (!m_bStopAll) { //do something. QCoreApplication::processEvents(); //Release the cup resource a moment. QThread::sleep(m_iInterval); }
说实话,当第一次用这个时,着实费脑子有些场景可能会想不通。如果类似调整参数或者收发消息之类的操作,应该是没问题的。但是这个子线程我是使用一个子窗体启动它的。而我希望一旦用户中途关闭子窗体,这个子线程也应该自动停止并关闭。
所以我在子窗体中加了这些:
FrmProducer::FrmProducer(QWidget *parent) : QMainWindow(parent), ui(new Ui::FrmProducer) { ui->setupUi(this); //构造函数中加这么一句,或者写在初始化函数中,目的是关闭窗体时候, //调用析构函数,因为默认是不调用的。多次踩坑。 setAttribute(Qt::WA_DeleteOnClose); } FrmProducer::~FrmProducer() { //问题一: //最初这里使用信号方式通知子线程结束,事实证明不可取。 //所以直接使用控制变量来作为while循环的跳出条件。 m_producer->m_bStopAll = true; //问题二: //释放子线程中的对象资源 if (nullptr != m_producer) { m_producer->deleteLater();//in event loop m_producer = nullptr; } //关闭线程并释放线程资源 if (nullptr != m_thd && m_thd->isRunning()) { m_thd->quit(); m_thd->wait(); m_thd->deleteLater();//in event loop m_thd = nullptr; } delete ui; }
上面的简化代码,大致说明意思即可。
其中释放资源的部分是需要思维清晰并理解深入的。
问题一:为了通知子线程结束流程,其实最先想到的是发送信号。但线程间通信,信号槽是异步的,亦即发送信号后,不是子线程马上执行槽函数,而是等待本线程先执行完,回到事件循环之后,子线程再执行槽函数。这有个调度顺序问题。
所以一定要考虑清除,发完信号之后的代码,千万不要提前断了子线程的后路,否则子线程的逻辑必然报错。
问题二:上面说了要注意发完信号之后的代码,所以我用了deletelater延迟销毁。但这个函数的含义是,等待子线程执行完逻辑并回到它的事件循环后,再执行销毁。这里不注意就尴尬了。
比如之前的代码:
while (!m_bStopAll) { //do something. //这里是事件循环,也就是执行到这里,父线程的deletelater会生效, //然后子线程的整个世界就毁灭了,所有操作必须安全退出,否则必然报错。 QCoreApplication::processEvents(); //出让cpu资源,顺便给m_bStopAll这种标记变量一个被外部更改的机会。 QThread::sleep(m_iInterval); }
既然事件循环用于信号和槽,也用于deletelater。那它是先执行结束while循环的响应还是deletelater呢?事件循环的说明是它会执行完队列中的所有该响应的信号。所以分析一下过程:
如果发送停止信号给子线程,然后马上写了deletelater来清理资源。它执行到事件循环时一定会先响应停止信号,并赋予标记变量m_bStop=true。然后马上清理资源。这就真尴尬了。
因为设置m_bStop=true是希望下一轮while时被检测然后退出while循环。但事实上还不等到下一轮,deletelater就被执行,子线程的世界在while循环停止前提前崩塌了,所以while依然会执行,只是之前的标记变量已经随着子线程的销毁而销毁,它的值已经不准了,所以极有可能造成无人看守的“野循环”。就好像野指针,这显然是不行的。
解决:所以我上面的方法是:
FrmProducer::~FrmProducer() { //直接使用控制变量来作为while循环的跳出条件。 m_producer->m_bStopAll = true; //释放子线程中的对象资源 if (nullptr != m_producer) { m_producer->deleteLater();//in event loop m_producer = nullptr; } //关闭线程并释放线程资源 if (nullptr != m_thd && m_thd->isRunning()) { m_thd->quit(); m_thd->wait(); m_thd->deleteLater();//in event loop m_thd = nullptr; } delete ui; }
子线程也要调整:
while (!m_bStopAll) { //在事件循环之前检测m_bStopAll这种标记变量, //这时候它不会因为deletelater销毁子线程后而无效 //事件循环用于响应信号和deletelater QCoreApplication::processEvents(); //耗时工作放在这里。 //出让cpu资源,顺便给m_bStopAll这种标记变量一个被外部更改的机会。 QThread::sleep(m_iInterval); }
这就没问题了。用于跳出while循环的标记变量,一定要在生效的时候去判断,所以一定要写在事件循环之前。
而这种标记变量的值,一定是在sleep的时候才有机会被外部更改。
所以上面的while的结束过程是这样的:
之前N轮循环完成工作。 sleep后外部改变m_bStop的值,标记生效。 下一轮开始while(m_bStop),循环退出。 -------------------------- 然后就没有然后了,它内部写的那句事件循环 QCoreApplication::processEvents(); 已经没机会执行。 因为while循环跳出后,子线程对象执行完毕, 回到了QThread的run函数中默认的exec()事件循环。 所以依然会执行资源清理: if (nullptr != m_worker) { m_worker->deleteLater();//in event loop m_worker = nullptr; } if (nullptr != m_thd && m_thd->isRunning()) { m_thd->quit(); m_thd->wait(); m_thd->deleteLater();//in event loop m_thd = nullptr; } 从而,子线程工作对象中的QCoreApplication::processEvents(); 已经不能导致deletelater在不恰当的时机发生。
所以,本人连续踩坑之后,发现还是自己理解不够深入,要说不懂也不是,但缺乏推敲,真的认真看了说明,也就想通了。