Qt多线程详解之QThread
关于多线程这个话题,相信大部分人都比较熟悉,但是要想深入理解多线程本质的话,多半得借助MCU以及RTOS这样的片上系统,今天就不深入展开,我们放在下一篇分析。从使用的角度,线程真的很棒很好用,特别是Qt库对多线程的封装,使得线程使用起来更加优雅,更少的代码和更简单的使用方式,今天就来详细分析qt的线程的使用方式以及应用场景等等
一、继承QThread重写run()
这种方式是最简单的,几乎在90%以上的场景都会用这个,非常适用于那种需要长期存在,也就是常驻内存的线程使用场景,使用非常简单,新建一个Mythread类,继承QThread,重写virtual void run();
,然后调用继承过来的共有函数start()
,线程就会开始运行,非常简单,下面看代码
//类定义
class CollectorThread : public QThread
{
Q_OBJECT
public:
CollectorThread(CthreadInterface *ptr);
signals:
public slots:
public:
void test() {
while(1)
{
}
}
protected:
void run() {//在cpp文件中,这个run函数一般是一个while循环
while(true) {
if(flag) {
//这里就实现所有的线程要做的事情,这个falg一般用于线程的工作的启动和停止的控制,也可以调用exec()阻塞,等待事件唤醒
}
}
}
private:
bool flag;
int currpoint = 0;
CthreadInterface *controlptr;
};
下面才是重点,大家有时候可能分不清线程之间的区别,假设有一个这样的场景,有一个按钮,按钮的槽函数中,需要间隔1秒打印5次,但是根据我们实际测试,如果在槽函数直接调用,类似下面这样
void on_pushButton_clicked() {
for(int i = 0; i < 5; i++) {
qDebug() << "my is main thread" << QThread::currentThreadId();
QThread::sleep(1);
}
}
//这样做会导致界面阻塞,不可点击拖动,也就是常见的卡死现象
- 重点重点重点
这时候说,我们写一个线程类,继承Qthread
,来执行这样的阻塞操作,可不可以呢,答案肯定是可以,但是有一个很大的误区,要分清楚主线程和子线程,以及当前函数到底在哪个线程中运行,有很多关于信号和槽又涉及多线程的时候,很多莫名其妙的错误,都是由于这个概念不清晰导致的
看下面的代码,由于篇幅有限,我将某些函数的实现也放在类中了,这是一个Qt默认生成的mainwindow界面,为了放在一起清晰,我直接两个类放在一起了
#include <QMainWindow>
#include <QThread>
#include <QDebug>
class TestTHread : public QThread//继承QThread
{
Q_OBJECT
public:
TestTHread(){}
void run(){//重写run,当调用start()时,就会调用这个函数
while(1) {
qDebug() << "1 my is run threadid" << QThread::currentThreadId();
QThread::sleep(10);
}
}
void test() {//这个函数,如果在run()中调用,就没有问题,但是在按钮槽函数中调用,界面同样会卡死
while(1) {
qDebug() << "2 my is test threadid" << QThread::currentThreadId();
sleep(10);
}
}
};
namespace Ui {
class MainWindow;
}
//*****************************************************************************************
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow()
{
thread.start();//界面的构造函数,会启动成员变量中创建的线程
}
~MainWindow();
private slots:
void on_pushButton_clicked() {
thread.test();
qDebug() << "3 my is main thread" << QThread::currentThreadId();
}
private:
TestTHread thread;//上面的自定义线程对象
Ui::MainWindow *ui;
};
//上面的代码一运行,我们注意看打印结果,由于贴图不方便,我直接将运行结果复制过来,大家注意看3条线程编号的提示信息
1 my is run thread 0x1a0c
2 my is run thread 0x12d8
3 my is main thread 0x12d8
- 重点:我们发现,只有
run()
函数中打印的线程ID是0x1a0c,而按钮的槽函数中的是0x12d8,按钮的槽函数中又调用了线程对象中的test()
函数,test()
函数是属于TestTHread
这个类中的函数,也就是线程对象中的函数,但是test()和槽函数中打印的线程ID一样 - 结论:当我们自己重写一个类,继承
QThread
,重写run()
函数的时候,只有run()
函数属于子线程,而这个类中的其他函数,如果在外部调用,还是属于在主线程中运行,也就表明,所有的阻塞操作,必须要在run()中完成,不然一样会导致界面卡死的现象
关于某个函数到底是在主线程运行,还是在子线程运行这个问题,其实从更底层的编译原理和C/C++语言的运行机制来看,函数其实并没有属于主线程还是子线程之分,代码被编译成汇编后,每个函数会获得一个标号,也可以理解为函数起始地址,判断一个函数到底在子线程还是主线程,跟函数本身没有关系,而是跟调用者有关系 , run()函数中调用的函数,就属于子线程,所以判别方式就是,看调用者,而不是看函数所处位置,子线程中调用的函数就属于子线程,主线程调用就属于主线程,。下面我给大家详细分析一下
run()
函数为什么会在子线程中呢?是因为当我们创建线程的时候,会在内存中分配一段单独的空间,代码只有一份,当调用start()
的时候,就会去从run()
这个函数的起始地址读取机器码,一条指令一条指令的读取运行,这个函数中的局部变量、以及函数调用,会被进行压栈,这个栈中就记录着当前线程运行的位置,也就是代码的位置,到底是运行到哪一条了呀,如果发生线程调用切换,就从这个专属的栈空间中弹栈,恢复代码运行位置、临时变量等等。因为子线程中调用的函数,局部变量,返回地址等也同样会被压栈进入子线程栈,反之主线程也一样,所以谁调用函数,函数就属于调用者所在线程。
所以如果有涉及阻塞的操作,一定要注意区分,阻塞操作到底是在哪里执行的,值得一提的是,信号和槽,是否在同一个线程,如果有这样的场景:在线程中发送信号,也就是在run()
中发送信号,而响应函数在主线程中,这种就属于跨线程传输,要注意connect()
是有第五个参数的,也就是如果发生跨线程的信号和槽,在哪里运行槽函数以及等待方式的问题。关于这个问题就不展开讨论,后续会详细分析。本篇就到这。
其实在学习的时候,大多数的场合都是最为简单的demo,没有这么复杂,但是在实际应用中,往往比这复杂得多,只有实际踩坑才会去注意这些具体应用的细节问题,关于线程的退出,等待并未过多提及,那个资料很多了,就不说了,贴个图
// 构造函数
QThread::QThread(QObject *parent = Q_NULLPTR);
// 判断线程中的任务是不是处理完毕了
bool QThread::isFinished() const;
// 判断子线程是不是在执行任务
bool QThread::isRunning() const;
// Qt中的线程可以设置优先级
// 得到当前线程的优先级
Priority QThread::priority() const;
void QThread::setPriority(Priority priority);
优先级:
QThread::IdlePriority --> 最低的优先级
QThread::LowestPriority
QThread::LowPriority
QThread::NormalPriority
QThread::HighPriority
QThread::HighestPriority
QThread::TimeCriticalPriority --> 最高的优先级
QThread::InheritPriority --> 子线程和其父线程的优先级相同, 默认是这个
// 退出线程, 停止底层的事件循环
// 退出线程的工作函数
void QThread::exit(int returnCode = 0);
// 调用线程退出函数之后, 线程不会马上退出因为当前任务有可能还没有完成, 调回用这个函数是
// 等待任务完成, 然后退出线程, 一般情况下会在 exit() 后边调用这个函数
bool QThread::wait(unsigned long time = ULONG_MAX);