引言
相比较于方法一,官方更加推荐方法二,如果大家想要了解方法一,可以阅读我的这一篇文章
写文章-CSDN创作中心
简介
我们在讲述这个方法之前,我们还是先简单的介绍一下这一种结构。重写run()函数的特点是创建一个线程,然后在这个线程里面去运行重写的run()函数,但是这里其实有一个问题,就是创建线程的开销其实还是比较大的,所以如果不停的创建线程,那么就会增大不必要的开销。
所以官方提供了一种新的机制就解决了这个问题,我们可以把这个线程想象成一艘船,然后我们要做的事情想象成工人,当船建立好之后,工人只要上船就可以工作,也就是说一个线程可以做A,也可以做B,只是工人做不同的事情在船上罢了。也就是说我们只需要一个线程,就可以做不同的事情,而且这个线程可以长期的存在,只用安排工人做不同的事情就可以了。
工作原理
这个方法的底层是信号和队列,也就是需要元对象系统的帮助。我们的子线程有一个事件队列,每当主线程发出信号,需要一个槽函数执行的时候,那么这个槽函数就会进入到事件队列里面,来等待操作系统的调度。这也就说明,不是说信号一发出,这个工人就开始工作,而是要等到操作系统(准确来说是进程的调度算法,因为操作系统不知道用户级线程的存在)来调度这个线程的时候这个工人才会去工作,去运行在事件队列里面等待的槽函数。OK,那既然这样,我们也就自然而然地明白了,这个事件队列里面可以排很多的槽函数,等待着被调度,一旦CPU去运行这个线程,那么槽函数们就可以依次上船,去让工人运行这些函数。
总结:我们这里只需要创建一艘船,然后多个槽函数,并让包含这些槽函数的工人上船,当操作系统调度这个线程的时候,工人就会从事件队列之中依次选出槽函数来执行,只要船不消失,那么工人就可以一直去工作,只是调度的问题。
当然了,一个线程(船)只能有一个工人,但是可以安排不同的工人上船,这就相当于线程池了,我们这里不考虑这一种情况
代码理解
我们还是通过一个例子去理解这个方法。
我们这里创建一个计时装置,可以通过按钮点击来控制计时装置的开始和停止。
#ifndef MAINWIN_H
#define MAINWIN_H
#include <QWidget>
#include <QPushButton>
#include <QLCDNumber>
#include <QThread>
#include "Worker.h"
class MainWin : public QWidget
{
Q_OBJECT
public:
MainWin(QWidget *parent = nullptr);
~MainWin() override;
public slots:
void do_lcd_show(int value);
void do_btn_work();
void do_task_over();
private:
void initWin();
QPushButton *m_btnStart;
QLCDNumber *m_lcdShow;
QThread *m_thread;
Worker *m_worker;
bool m_isRunning;
};
#endif
这个是GUI线程的实现,里面要解释的一个是Worker *m_worker;这个是我们创建的工人,去运行他包含的槽函数,QThread *m_thread;这个是我们创建的船,毕竟船不是凭空出现的嘛~~
然后对应的槽函数我们之后会一一解释。
我们开始创建一个可以干活的工人,并且在里面安排它可以干的活
#ifndef WORKER_H
#define WORKER_H
#include <QObject>
#include <atomic>
class Worker : public QObject{ // 这些活是通过信号的槽的方式通知
Q_OBJECT
public:
explicit Worker(int max, QObject* parent = nullptr);
~Worker() override;
public slots:
void doWork(); // 开始数数的工作
void doExit(); // 停止数数的工作
signals:
void valueReady(int value); // 发出生产者生产产品的信号
void overReady(); // 发出生产者结束生产的信号
private:
int m_max;
//bool task_running = false;
std::atomic<bool> task_running;
};
#endif
然后安排工人干活
#include "Worker.h"
#include <QDebug>
#include <QThread>
Worker::Worker(int max, QObject* parent) : QObject(parent), m_max(max)
{
}
Worker::~Worker(){
qDebug() << __PRETTY_FUNCTION__;
}
void Worker::doWork()
{
qDebug() << "工人工作的线程ID号:" << QThread::currentThreadId();
int value = 0;
task_running.store(true);
while(task_running.load() && value <= m_max){
emit valueReady(value++);
QThread::msleep(100);
}
if(task_running.load()){
qDebug() << "Worker normal over!";
} else {
qDebug() << "Worker exit!";
}
emit overReady();
}
void Worker::doExit()
{
qDebug() << "准备停止任务" << QThread::currentThreadId();
// task_running = false;
task_running.store(false);
}
我们简单的解释一下工人的事情:
因为这里存在着线程之间的切换,我们就必须要考虑到公共变量的访问问题,所以这里的std::atomic<bool> task_running 我们设置的是原子变量。然后每一次加加这个数的时候,会发送信号给lcd屏幕,然后都会经过while()循环来判断一下这个 task_running有没有被GUI的主线程改变,这个变量改变的方式也就是主线程会发出一个信号,让doExit()进入事件队列,被工人执行,然后变成false,当变成false了之后,工人就会发出信号overReady(),让GUI线程知道,停止显示数字变化。
我相信大家还注意到了一个线程ID号的事情

这也可以证明我们的结构,工人和船的ID号虽然不一样,但是都是不变的。
最后是GUI线程的实现,这里主要就是当线程结束的时候,我们需要销毁这个船,释放内存,然后点击按钮事件的时候,要执行对应的槽函数。
#include "MainWin.h"
#include "Worker.h"
#include <QVBoxLayout>
#include <QPushButton>
#include <QLCDNumber>
#include <QDebug>
MainWin::MainWin(QWidget *parent) : QWidget(parent),
m_btnStart(nullptr), m_lcdShow(nullptr), m_thread(new QThread(this)), m_worker(nullptr), m_isRunning(false)
{
initWin();
qDebug() << "GUI线程的ID号:" << QThread::currentThreadId();
m_worker = new Worker(30); // 注意这个工人不可以有父对象,如果属于this,那么这个工人就属于这个图形的线程了
m_worker->moveToThread(m_thread);
m_thread->start(); // 当前m_thread进入到默认的run方法执行exec,开启线程的事件循环
connect(m_worker, &Worker::valueReady, this, &MainWin::do_lcd_show);
connect(m_worker, &Worker::overReady, this, &MainWin::do_task_over);
connect(m_thread, &QThread::finished, m_worker, &QObject::deleteLater);
connect(m_btnStart, &QPushButton::clicked, this, &MainWin::do_btn_work); // 编译器会判断这两个对象是不是在一个进程里面,如果不在,就自动放在事件队列里
}
MainWin::~MainWin()
{
qDebug() << __PRETTY_FUNCTION__;
m_thread->quit(); // 一般都是quit,因为默认退出时候的状态是0状态码
m_thread->wait();
}
void MainWin::do_lcd_show(int value)
{
m_lcdShow->display(value);
}
void MainWin::do_btn_work()
{
if(!m_isRunning){
// 用元对象系统提供的invokeMethod方法调用doWork槽函数
QMetaObject::invokeMethod(m_worker, "doWork", Qt::QueuedConnection);
m_btnStart->setText("停止");
m_isRunning = true;
} else {
// QMetaObject::invokeMethod(m_worker, "doExit", Qt::QueuedConnection);
m_worker->doExit(); // 由主线程改变全局变量m_isRunning,通知工人停止工作,而上面那一种方法,是直接调用doExit槽函数,但是这个函数是在队列里面执行的,所以会等doWork槽函数执行完才会执行
}
}
void MainWin::do_task_over()
{
m_btnStart->setText("开始");
m_isRunning = false;
}
void MainWin::initWin()
{
auto main_layout = new QVBoxLayout(this);
m_btnStart = new QPushButton("开始", this);
m_lcdShow = new QLCDNumber(this);
main_layout->addWidget(m_lcdShow);
main_layout->addWidget(m_btnStart);
this->setLayout(main_layout);
this->resize(300, 300);
}
相信聪明的你,已经看到了在do_btn_work()函数中,有一个 QMetaObject::invokeMethod()函数,这个是系统封装的函数,作用就是通知工人该干什么活了。
如果大家真的了解了元对象系统的原理,就会知道它是根据字符串来查找一个类是否有相应的属性,所以我们这里传递的是字符串“”,而最后一个参数就是交代我们是使用的是事件队列来实现的。
最后最后,还有一点很关键,关于释放的问题。我们释放线程不代表工人就被释放了,所以当线程结束的时候,同时还需要释放工人,那么也就有了connect(m_thread, &QThread::finished, m_worker, &QObject::deleteLater);
那线程应该怎么释放呢?子线程已经加入了树的维护,然后主线程的析构函数调用的wait会等待子线程释放完再释放自己。
总结
我们还是最后总结一下关键的知识点:
moveToThread(),这个是任何一个继承了QObejct都有的一个函数,调用这个函数相当于工人上了船,不过前提是先创建一个线程,因为这个函数要传一个线程的指针。
然后船有了,但是要启动,所以不要忘记了调用start()函数。
最后唤醒工人做相对应的活,利用的是元对象系统,QMetaObject::invokeMethod()
这就是第二种方法,全程只创建了一个线程,却可以干很多的事情。
感谢大家的阅读,希望可以帮助到大家更好的理解!!!





