技术: Qt 线程案例汇总

集中梳理一下简单的 Qt 线程案例。

不管怎么看,Qt 框架对于线程的封装和Java倒是挺像的.

并发这里主要讲线程,但是线程这一块儿本身内容就很多,这里不讲QRunnable, QConcurrent等。

只介绍GUI编程最常用的,深入的东西,请参考文档。

线程简介

哪些地方用到了线程?
避免耗时操作导致主程序无响应,所以开辟新县城处理耗时任务。
耗时任务:

  • 网络文件读写
  • UI 绘制, 控件组装

下面这段代码,如果在主线程,即 GUI 线程执行,那么肯定会造成界面卡死:

1
2
3
4
5
6
7
8
9
10
//如果定时器没有开始工作,那么久开启它
if (m_Timer->isActive() == false) {
m_Timer->start(100); //100毫秒触发一次定时任务
}
//在主线程里面执行耗时任务, 例如
QThread::sleep(5);

//处理完毕后,关闭定时器
m_Timer->stop();
qDebug() << "over";

线程详解

QT4 线程

这个比较简单,依托于继承体系, 这就是你要去单独写一个线程类,在里面完成任务。

核心逻辑用下来这个图讲解了:

这种方式非常像 java 的其中一种线程。

归纳起来就是:

  • run方法就是线程执行体
  • 启动线程需要这样的一个线程实例(QThread子类实例, 主线程保留,因为还要为其收尸)
  • 启动线程不能调用run, 而必须使用 start 方法 (start 方法可以指定优先级)
  • run 方法里面发送信号,通知相关绑定的方法,任务已经完成
  • 主线程收尸 (join之类的)

这就是常见的逻辑。

给个例子, 核心逻辑大概如下:
(线程的 run 方法定义和上面说的逻辑差不多)

工作线程类的实现(run方法):

之后绑定任务:

dealDone槽函数是在 GUI 线程定义的,而信号是在非GUI线程定义和发送的,所以connect绑定的时候,应该指定队列的方式绑定。
这里采用的 auto 方式,即默认方式,自动就按照多线程的逻辑处理好了,所以不用担心。

计时器因为计时器实在主线程启动的,所以这里计时器也只能在主线程内工作,即触发计时器绑定的槽函数。

Qt4计时器案例

这里写了一个案例, LCD数字显示器的计时,大致思路和上面差不多,直接贴代码了。

线程类设计如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <QThread>

class MyThread : public QThread
{
Q_OBJECT
public:
explicit MyThread(QObject *parent = nullptr);

protected:
void run(); //线程处理函数 (虚函数),但不直接调用

signals:
void isDone(); //告诉别人我这个线程的任务已经完成了


public slots:
};

//run方法实现
void MyThread::run()
{
//很复杂的数据处理 (耗时操作)
QThread::sleep(5); //耗时5秒

emit isDone(); //发送信号
}

工作界面类如下: (主线程,主界面类)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public:
void dealTimer(); //定时器触槽函数
void dealDone(); //处理线程完毕的槽函数

private slots:
void on_pushButton_clicked();

private:
Ui::Widget *ui;
QTimer *timer;

int count; //lcd控件 计数

MyThread *thread; //线程对象

//实现逻辑如下:
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>
#include <QThread> //for sleep()


Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);

setWindowTitle("耗时任务案例");

timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, &Widget::dealTimer);

thread = new MyThread(this);
connect(thread, &MyThread::isDone, this, &Widget::dealDone);
}

void Widget::on_pushButton_clicked()
{
count = 0;
if (!timer->isActive()) {
timer->start(50); //50毫秒触发一次计时,大致显示到数字100
}

//开启新线程处理耗时任务
qDebug() << "开启新线程任务";
thread->start(); //线程优先级用默认参数就好

}

//定时器槽函数,负责重置 UI LCD Number
void Widget::dealTimer()
{
count++;
ui->lcdNumber->display(count);
}

//子线程发信号过来就触发,然后结束线程
void Widget::dealDone()
{
qDebug() << "线程任务完毕了,关闭计时器, 结束线程" ;
timer->stop();
thread->quit(); //或者在窗口的 destroy 信号槽中处理 (但不要用 terminate, 这回导致动态分配资源泄露)

//主线程对于工作线程的一种回收,处理;确保工作线程完毕
thread->wait();
}

Widget::~Widget()
{
delete ui;
}

运行效果大致如下:

Qt5 线程

上面一种原始的线程方式,虽然简单,但是灵活。

其实我们原始的初衷是,我只是想让这部分代码或者这个耗时操作单独开一个线程执行,即让一个方法跑在非GUI线程而已。

Qt线程提供了 Java 中类似 Runnable 的方式, 非常灵活。

一般实现和使用的思路大致如下:

线程类可以是一个随便的类(一般叫做 worker 类),只要 moveToThread()即可。

为什么开启子线程后需要用信号和槽的方式去启动线程执行体,而不是直接启动?

  • 直接 start 就相当于 GUI 线程直接调用这个方法(虽然把那个对象放入了线程中)
  • 通过槽启动,其实是为了利用槽函数的 sender 和 receiver不再一个线程,可以用自动连接的方式在另外一个线程运行那个执行体.

最后注意两点:

  • 千万注意,调用线程执行体,一定要用信号和槽去调用(主线程发信号,槽函数绑定为线程执行体)
  • 自定义类Worker用于线程执行体的实例,就不能指定父对象,因为后面要 worker.moveToThread(QThread *); 所以不能给他定义主线程对象去管理它的声明周期

Qt5计时器案例

同样的计时处理,这次用 这种灵活的方式来处里,核心代码如下:(灵活的代价是,代码量显著增加了)

woker类的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class MyThread : public QObject
{
private:
bool isStop; //循环终止判断标识
public:

void myTimeout();//线程执行体
void setFlag(bool flag = true);//让外部可以终止线程执行体里面的循环

signals:
void myTimeoutSignal(); // 线程执行体里面发信号通知主线程 (每隔1秒发送一个信号)
};

/********具体实现代码******/
MyThread::MyThread(QObject *parent) : QObject(parent)
{
isStop = false; //一开始肯定不能停止呀
}

//线程执行体,不断的发送信号
void MyThread::myTimeout()
{
qDebug() << "子线程号: " << QThread::currentThread();

while(!isStop) {
QThread::msleep(100);
emit myTimeoutSignal();
}
}


void MyThread::setFlag(bool flag)
{
isStop = flag;
}

主线程的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
class Widget : public QWidget
{
Q_OBJECT

public:
explicit Widget(QWidget *parent = 0);
~Widget();

void dealTimeoutSignal(); // 处理子线程发过来的信号

signals:
void startThreadInstance(); //发送信号,启动线程

private slots:
void on_btnStart_clicked(); //按钮操作触发停止或者开启计时
void on_btnStop_clicked();

private:
Ui::Widget *ui;

MyThread *threadInstance;
QThread *thread;

int count;
};

/*******具体实现,如下: ********/
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);

//注意线程对象不要指定父对象 (我们自己控制它的生命周期)
threadInstance = new MyThread; //如果指定了父对象,后面就不能再moveToThread()了

//创建子线程 (把 Mythread类对象关联过去)
thread = new QThread(this); //真正的线程要关联父对象
threadInstance->moveToThread(thread); //把 worker类关联到线程

//这个绑定的是,线程执行体执行起来之后的操作 (非主线程发消息)
connect(threadInstance, &MyThread::myTimeoutSignal, this, &Widget::dealTimeoutSignal);
qDebug() << "主线程号: " << QThread::currentThread();

//不要直接调用,而是要通过信号和槽去调用线程执行体myTimeout
connect(this, &Widget::startThreadInstance, threadInstance, &MyThread::myTimeout);

//关闭窗口的时候回收线程资源
connect(this, &Widget::destroyed, this, [=](){
if (thread->isRunning()) {
threadInstance->setFlag(true); //先停止循环 (threadInstance 这个 woker实例已经moveThread了)
thread->quit(); //线程执行体里面是死循环,如果不停, 则退不出去
thread->wait();

delete threadInstance;

qDebug() << "终止子线程";
}
});
}

Widget::~Widget()
{
delete ui;
}

//子线程发送信号myTimeoutSignal过来,就执行这个槽函数
void Widget::dealTimeoutSignal()
{
count++;
ui->lcdNumber->display(count);
}

void Widget::on_btnStart_clicked()
{
if (thread->isRunning()) {
qDebug() << "已经运行啦,还点它做啥?";
return;
}

count = 0;
thread->start(); //开启线程, 但是没有启动线程执行体 myTimeout()
threadInstance->setFlag(false); //stop之后又点击 start按钮
//emit 通过信号和槽调用启动线程执行体 (而不是去直接调用, 否则就是同一个线程执行了)
//threadInstance->myTimeout();//发现线程号还是主线程的
emit startThreadInstance(); //调用线程执行体
qDebug() << "开启子线程";

}

void Widget::on_btnStop_clicked()
{
if (!thread->isRunning()) {
qDebug() << "都没有在跑, 停止啥?";
return;
}
//停止线程 quit是退出, wait是让主线程回收子线程资源
//否则 QThread: Destroyed while thread is still running
threadInstance->setFlag(true); //先停止循环
thread->quit(); //线程执行体里面是死循环,如果不停, 则退不出去
thread->wait();
qDebug() << "终止子线程";
}

貌似并没有第一种方式简单,其实并不是,核心逻辑还是在不同线程的两个对象,通过信号槽启动线程执行体。你可以先不管这个线程执行体在干啥,其他它也简单,它要让LCD Number的数字改变,但是他不能操作UI,所以他只能再发信号给主线程,让主线程变一下UI。

核心逻辑其实很简单。

运行截图:

线程综合案例

以前在总结网络的时候,已经说了一个 QNetworkManager 同步下载案例,那个案例里面下载操作就是通过线程实现的网络文件读写。这里说另外一种耗时操作,UI绘制

线程函数不允许操作图形界面,那么怎么绘制?

确实,子线程不能操作图形界面,但这仅仅意味着不能再UI控件上显示图形&绘制推行,但是它可以做图形的数据处理,IO读写。
如果图形是从文件里读取的或者根据具体的平台模拟生成的,这个时候如果放在主线程里面做这个耗时操作,显然会造成界面假死。

那么可以把这些图形读取,或者图形数据的生成等任务交给子线程,然后传递给主线程就可以了。

下面这个案例,就借助 QImage 这个图像载体实现跨线程的图形传输任务,主要思路如下:

主线程任务: 开启子线程(并调用绘图线程体),接收子线程发来的图片
子线程的任务: 绘图,发送图片主线程(借助信号发送)

期间都是通过信号和槽机制实现的。

具体的代码,大致如下:

worker类的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class Worker : public QObject
{
Q_OBJECT
public:
explicit Worker(QObject *parent = nullptr);

//线程绘图
void run();

signals:
void sendImage(QImage image); //发送信号,让主线程接收图片
};

/***** 线程执行体的实现 **/
//如果很复杂的图片,那么交给子线程来画,最好不过了
void Worker::run()
{
//定义QImage对象
QImage image(300, 300 , QImage::Format_ARGB32); //格式带有透明背景
//定义绘图器
QPainter p(&image);


//定义画笔
QPen pen;
pen.setWidth(5); //设置宽度
p.setPen(pen); //画笔交给绘图器

//定义画刷
QBrush brush;
brush.setStyle(Qt::CrossPattern); //样式
brush.setColor(Qt::green);
p.setBrush(brush); //交给绘图器


qsrand(QTime::currentTime().second());

//定义5个点 (多边形)
QPoint a[] = {
QPoint(qrand()%300, qrand()%300),
QPoint(qrand()%300, qrand()%300),
QPoint(qrand()%300, qrand()%300),
QPoint(qrand()%300, qrand()%300),
QPoint(qrand()%300, qrand()%300),
}; //5个点都在范围内

p.drawPolygon(a, 5); //绘制多边形 (这里面API很多, 还可以有其他方式)

emit sendImage(image); //发信号,让主线程接收图片
}

主线程核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public:
void recvImage(QImage); //收取图片的槽函数 (因为对方的信号有参数,所以槽函数也要有)

protected:
void paintEvent(QPaintEvent *); //重写绘图事件

private:
Ui::Widget *ui;

Worker *worker;
QThread *thread;
QImage image; //用于存储子线程绘制的图片


/******** 相关实现 *******/
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);

worker = new Worker; //不要指定父对象
thread = new QThread(this);
worker->moveToThread(thread); //把自定义模块添加到线程

thread->start(); //启动线程, 但是没有设置线程执行体

//绑定按钮点击 (让子线成内开始绘图,并发送回来给主(本)线程)
connect(ui->pushButton, &QPushButton::pressed, worker, &Worker::run);

//子线程发送过来的图片处理一下
connect(worker, &Worker::sendImage, this, &Widget::recvImage);


//处理一下关闭窗口事件
connect(this, &Widget::destroyed, [=](){
if (thread->isRunning()) {
thread->quit();
thread->wait();
}
delete worker;
});
}

Widget::~Widget()
{
delete ui;
}

/*收到子线程的信号,开始处理信号: 调用重新绘制*/
void Widget::recvImage(QImage image)
{
this->image = image;
//update(); //更新窗口(重绘时自动调用 paintEvent)
repaint();
}

void Widget::paintEvent(QPaintEvent *)
{
//画在窗口上面,所以指定绘图设备为 this
QPainter p(this); //这个绘制 API 在 macOS下貌似总是会在上一份图形的基数上绘制
p.drawImage(50, 50, image);
}

运行的效果图如下:

(总是无法覆盖第一次的图形,可能是因为绘图器上必须至少要有一个图形)

文章目录
  1. 1. 线程简介
  2. 2. 线程详解
    1. 2.1. QT4 线程
    2. 2.2. Qt4计时器案例
    3. 2.3. Qt5 线程
    4. 2.4. Qt5计时器案例
  3. 3. 线程综合案例
|