个人学习多线程控制,写了一些博文用于记录。
【Qt线程-1】this,volatile,exec(),moveToThread()
【Qt线程-3】使用事件循环,信号,stop变量,sleep阻塞,QWaitCondition+QMutex条件变量,退出子线程工作
【Qt线程-4】事件循环嵌套,BlockingQueuedConnection与QWaitCondition比较
【Qt线程-5】生产者&消费者模型应用(多态,子线程控制,协同,事件循环)
【Qt线程-6】获取当前线程id,thread()和currentThreadId(),不是想当然那样,不使用信号槽可能看不出区别
以前用vb或者c#的时候,需要响应哪个事件,就可以添加响应函数来做相应处理。vc++的mfc也是如此。但是它内部是如何实现的,即时暂时不知道,大多数情况下也没有影响做项目。但是在qt中,消息和槽成为了比较通用的概念,很多时候更善于利用它进行参数传递。但事实上,消息和槽机制,离不开事件循环。
所谓事件循环,就是循环执行消息响应,此时一旦消息队列有消息发生,就可以马上执行槽响应函数。
QCoreApplication::processEvents。qt手册这样说的:
Processes all pending events for the calling thread according to the specified flags until there are no more events to process.
You can call this function occasionally when your program is busy performing a long operation (e.g. copying a file).
In the event that you are running a local loop which calls this function continuously, without an event loop, the DeferredDelete events will not be processed. This can affect the behaviour of widgets, e.g. QToolTip, that rely on DeferredDelete events to function properly. An alternative would be to call sendPostedEvents() from within that local loop.
Calling this function processes events only for the calling thread, and returns after all available events have been processed. Available events are events queued before the function call. This means that events that are posted while the function runs will be queued until a later round of event processing.
以我这个英语不咋地的水平理解,就是处理消息队列中的所有消息。所谓消息,就好像微软常说的事件,比如点击一个按钮,可以写它的响应事件。qt里是信号和槽。这东西大多是异步的,别人给它发消息,都是先入队列,待会儿它会调用这个函数统一处理。流程为了保持响应,需要轮巡处理消息事件,所以就叫事件循环。上述英文也说了,耗时操作要时不时调用它一下,就像游泳要喘气。
就如常见的main函数,主窗体打开后,后面有个exec(),这就是事件循环。可以理解为一个死循环,永远在等待消息队列。所以,在窗体中放个控件,才可以有槽函数来处理信号响应。
#include "mainwindow.h"
#include
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();//事件循环
}
如果没有它,主窗体会闪一下就过去了。比如这样:
#include "mainwindow.h"
#include
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return 0;
//return a.exec();
}
像这样main函数以很快的速度创建一个窗体并显示一下,然后又顺理成章地返回0,主进程随着main函数的结束而结束,窗体只是闪一下,什么都干不了。
写多线程的时候,可以从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在不恰当的时机发生。
所以,本人连续踩坑之后,发现还是自己理解不够深入,要说不懂也不是,但缺乏推敲,真的认真看了说明,也就想通了。
其实还有另外的场景和方法:
【Qt线程-3】使用事件循环,信号,stop变量,sleep阻塞,QWaitCondition+QMutex条件变量,退出子线程工作_大橘的博客-CSDN博客