Qt----多线程

文章目录

  • 多线程
  • 一、继承QThread的线程
    • 1. 应用实例
    • 2. 程序运行效果
  • 二、继承QObject的线程
    • 1. 应用实例
    • 2. 程序运行效果

多线程

我们写的一个应用程序,应用程序跑起来后一般情况下只有一个线程,但是可能也有特殊情况。比如我们前面章节写的例程都跑起来后只有一个线程,就是程序的主线程。线程内的操作都是顺序执行的。恩,顺序执行?试着想一下,我们的程序顺序执行,假设我们的用户界面点击有某个操作是比较耗时的。您会发现界面点击完了,点击界面对应的操作还没有完成,所以就会冻结界面,不能响应,直到操作完成后,才返回到正常的界面里。如果我们的界面是这么设计的话,估计用户得发毛了。

这种情况我们一般是创建一个单独的线程来执行这个比较耗时的操作。比如我们使用摄像头拍照保存照片。恩,很多朋友问,这个不算耗时吧。对的在电脑上使用 Qt 拍照,处理起来非常快。根本也不需要开启一个线程来做这种事。但是我们是否考虑在嵌入式的 CPU 上做这种事情呢?嵌入式的 CPU 大多数都没有电脑里的 CPU 主频(几 GHz)那么高,处理速度也不快。此时我们就需要考虑开多一个线程来拍照了。拍完照再与主线程(主线程即程序原来的线程)处理好照片的数据,就完成了一个多线程的应用程序了。

官方文档里说,QThread 类提供了一种独立于平台的方法来管理线程。QThread 对象在程序中管理一个控制线程。QThreads 在 run()中开始执行。默认情况下,run()通过调用 exec()来启动事件循环,并在线程中运行 Qt 事件循环。您可以通过使用 QObject::moveToThread()将 worker对象移动到线程来使用它们。

QThread 线程类是实现多线程的核心类。Qt 有两种多线程的方法,其中一种是继承 QThread的 run()函数,另外一种是把一个继承于 QObject 的类转移到一个 Thread 里。Qt4.8 之前都是使用继承 QThread 的 run()这种方法,但是 Qt4.8 之后,Qt 官方建议使用第二种方法。两种方法区别不大,用起来都比较方便,但继承 QObject 的方法更加灵活。所以 Qt 的帮助文档里给的参考是先给继承 QObject 的类,然后再给继承 QThread 的类。

另外 Qt 提供了 QMutex、QMutexLocker、QReadLocker 和 QWriteLocker 等类用于线程之间的同步,详细可以看 Qt 的帮助文档。

本章介绍主要如何使用 QThread 实现多线程编程,讲解如何通过继承 QThread 和 QObject的方法来创建线程。还会使用 QMutexLocker 正确的退出一个线程。本章的内容就是这么多,并不深入,所以不难, 目的就是握 快速掌握 Qt

一、继承QThread的线程

在第十章的章节开头说过了,继承 QThread 是创建线程的一个普通方法。其中创建的线程只有 run()方法在线程里的。其他类内定义的方法都在主线程内。恩,这样不理解?我们画个图捋一捋。
Qt----多线程_第1张图片
通过上面的图我们可以看到,主线程内有很多方法在主线程内,但是子线程,只有 run()方法是在子线程里的。run()方法是继承于QThread 类的方法,用户需要重写这个方法,一般是把耗时的操作写在这个 run()方法里面。

1. 应用实例

本例目的:快速了解继承 QThread 类线程的使用。

例 05_qthread_example1 , 继 承 QThread 类 的线 程 ( 难 度 :一 般)。项 目 路 径 为Qt/2/05_qthread_example1。本例通过 QThread 类继承线程,然后在 MainWindow 类里使用。通过点击一个按钮开启线程。当线程执行完成时,会发送resultReady(const QString &s)信号给主线程。流程就这么简单。

在头文件“mainwindow.h”具体代码如下。

/******************************************************************
Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
* @projectName 05_qthread_example1
* @brief mainwindow.h
* @author Deng Zhimao
* @email [email protected]
* @net www.openedv.com
* @date 2021-04-06
*******************************************************************/
1 #ifndef MAINWINDOW_H
2 #define MAINWINDOW_H
3
4 #include <QMainWindow>
5 #include <QThread>
6 #include <QDebug>
7 #include <QPushButton>
8
9 /* 使用下面声明的 WorkerThread 线程类 */
10 class WorkerThread;
11
12 class MainWindow : public QMainWindow
13 {
14 Q_OBJECT
15
16 public:
17 MainWindow(QWidget *parent = nullptr);
18 ~MainWindow();
19
20 private:
21 /* 在 MainWindow 类里声明对象 */
22 WorkerThread *workerThread;
23
24 /* 声明一个按钮,使用此按钮点击后开启线程 */
25 QPushButton *pushButton;
26
27 private slots:
28 /* 槽函数,用于接收线程发送的信号 */
29 void handleResults(const QString &result);
30
31 /* 点击按钮开启线程 */
32 void pushButtonClicked();
33 };
34
35 /* 新建一个 WorkerThread 类继承于 QThread */
36 class WorkerThread : public QThread
37 {
38 /* 用到信号槽即需要此宏定义 */
39 Q_OBJECT
40
41 public:
42 WorkerThread(QWidget *parent = nullptr) {
43 Q_UNUSED(parent);
44 }
45
46 /* 重写 run 方法,继承 QThread 的类,只有 run 方法是在新的线程里 */
47 void run() override {
48 QString result = "线程开启成功";
49
50 /* 这里写上比较耗时的操作 */
51 // ...
52 // 延时 2s,把延时 2s 当作耗时操作
53 sleep(2);
54
55 /* 发送结果准备好的信号 */
56 emit resultReady(result);
57 }
58
59 signals:
60 /* 声明一个信号,译结果准确好的信号 */
61 void resultReady(const QString &s);
62 };
63
64 #endif // MAINWINDOW_H
65

第 36 行,声明一个 WorkerThread 的类继承 QThread 类,这里是参考 Qt 的 QThread 类的帮助文档的写法。
第 47 行,重写 run()方法,这里很重要。把耗时操作写于此,本例相当于一个继承 QThread类线程模板了。

在源文件“mainwindow.cpp”具体代码如下。

/******************************************************************
Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
* @projectName 05_qthread_example1
* @brief mainwindow.cpp
* @author Deng Zhimao
* @email [email protected]
* @net www.openedv.com
* @date 2021-04-06
*******************************************************************/
1 #include "mainwindow.h"
2
3 MainWindow::MainWindow(QWidget *parent)
4 : QMainWindow(parent)
5 {
6 /* 设置位置与大小 */
7 this->setGeometry(0, 0, 800, 480);
8
9 /* 对象实例化 */
10 pushButton = new QPushButton(this);
11 workerThread = new WorkerThread(this);
12
13 /* 按钮设置大小与文本 */
14 pushButton->resize(100, 40);
15 pushButton->setText("开启线程");
16
17 /* 信号槽连接 */
18 connect(workerThread, SIGNAL(resultReady(QString)),
19 this, SLOT(handleResults(QString)));
20 connect(pushButton, SIGNAL(clicked()),
21 this, SLOT(pushButtonClicked()));
22 }
23
24 MainWindow::~MainWindow()
25 {
26 /* 进程退出,注意本例 run()方法没写循环,此方法需要有循环才生效 */
27 workerThread->quit();
28
29 /* 阻塞等待 2000ms 检查一次进程是否已经退出 */
30 if (workerThread->wait(2000)) {
31 qDebug()<<"线程已经结束!"<<endl;
32 }
33 }
34
35 void MainWindow::handleResults(const QString &result)
36 {
37 /* 打印出线程发送过来的结果 */
38 qDebug()<<result<<endl;
39 }
40
41 void MainWindow::pushButtonClicked()
42 {
43 /* 检查线程是否在运行,如果没有则开始运行 */
44 if (!workerThread->isRunning())
45 workerThread->start();
46 }

第 11 行,线程对象实例化,Qt 使用 C++基本都是对象编程,Qt 线程也不例外。所以我们也是用对象来管理线程的。
第 24~33 行,在 MainWindow 的析构函数里退出线程,然后判断线程是否退出成功。因为我们这个线程是没有循环操作的,直接点击按钮开启线程后,做了 2s 延时操作后就完成了。所以我们在析构函数里直接退出没有关系。
第 41~46 行,按钮点击后开启线程,首先我们得判断这个线程是否在运行,如果不在运行我们则开始线程,开始线程用 start()方法,它会调用重写的 run()函数的。

2. 程序运行效果

点击开启线程按钮后,延时 2s 后,Qt Creator 的应用程序输出窗口打印出“线程开启成功”。在 2s 内多次点击按钮则不会重复开启线程,因为线程在这 2s 内还在运行。同时我们可以看到点击按钮没卡顿现象。因为这个延时操作是在我们创建的线程里运行的,而 pushButton 是在主线程里的,通过点击按钮控制子线程的运行。

当关闭程序后,子线程将在主线程的析构函数里退出。注意线程使用 wait()方法,这里等待 2s,因为我们开启的线和是延时 2s 就完成了。如果是实际的操作,请根据 CPU 的处理能力,给一个适合的延时,阻塞等待线程完成后,就会自动退出并打印“线程已经结束”。
Qt----多线程_第2张图片

二、继承QObject的线程

开头已经说过,继承 QThread 类是创建线程的一种方法,另一种就是继承QObject 类。继承 QObject 类更加灵活。它通过QObject::moveToThread()方法,将一个 QObeject的类转移到一个线程里执行。恩,不理解的话,我们下面也画个图捋一下。
Qt----多线程_第3张图片
通过上面的图不难理解,首先我们写一个类继承 QObject,通过 QObject::moveToThread()方法将它移到一个 QThread 线程里执行。那么可以通过主线程发送信号去调用 QThread 线程的方法如上图的 fun4(),fun5()等等。这些方法都是在 QThread 线程里执行的。

1. 应用实例

本例目的:快速了解继承 QObject 类线程的使用。

例 06_qthread_example2 , 继 承 QObject 类 的 线 程 ( 难 度 : 一 般 )。 项 目 路 径 为Qt/2/06_qthread_example2。本例通过 QObject 类继承线程,然后在 MainWindow 类里使用。通过点击一个按钮开启线程。另一个按钮点击关闭线程。另外通过加锁的操作来安全的终止一个线程。(我们可以通过 QMutexLocker 可以安全的使用 QMutex 以免忘记解锁。)

在我们谈谈为什么需要加锁来终止一个线程?因为 quit()和 exit()方法都不会中途终止线程。要马上终止一个线程可以用 terminate()方法。但是这个函数存在非常不安全的因素,Qt 官方文档说不推荐使用。

我们可以添加一个 bool 变量,通过主线程修改这个 bool 变量来终止,但是有可能引起访问冲突,所以需要加锁,例程里可能体现不是那么明确,当我们有 doWork1(),doWork2…就能体现到 bool 变量加锁的作用了。但是加锁会消耗一定的性能,增加耗时。

下面的例子是仿照 Qt 官方写的,看似简单,但是流程大家可能不是很明白,所以画个了大体的流程图,给大伙瞧瞧。
Qt----多线程_第4张图片
在头文件“mainwindow.h”具体代码如下。

1 #ifndef MAINWINDOW_H
2 #define MAINWINDOW_H
3
4 #include <QMainWindow>
5 #include <QThread>
6 #include <QDebug>
7 #include <QPushButton>
8 #include <QMutexLocker>
9 #include <QMutex>
10
11 /* 工人类 */
12 class Worker;
13
14 class MainWindow : public QMainWindow
15 {
16 Q_OBJECT
17
18 public:
19 MainWindow(QWidget *parent = nullptr);
20 ~MainWindow();
21
22 private:
23 /* 开始线程按钮 */
24 QPushButton *pushButton1;
25
26 /* 打断线程按钮 */
27 QPushButton *pushButton2;
28
29 /* 全局线程 */
30 QThread workerThread;
31
32 /* 工人类 */
33 Worker *worker;
34
35 private slots:
36 /* 按钮 1 点击开启线程 */
37 void pushButton1Clicked();
38
39 /* 按钮 2 点击打断线程 */
40 void pushButton2Clicked();
41
42 /* 用于接收工人是否在工作的信号 */
43 void handleResults(const QString &);
44
45 signals:
46 /* 工人开始工作(做些耗时的操作 ) */
47 void startWork(const QString &);
48 };
49
50 /* Worker 类,这个类声明了 doWork1 函数,将整个 Worker 类移至线程 workerThread
*/
51 class Worker : public QObject
52 {
53 Q_OBJECT
54
55 private:
56 /* 互斥锁 */
57 QMutex lock;
58
59 /* 标志位 */
60 bool isCanRun;
61
62 public slots:
63 /* 耗时的工作都放在槽函数下,工人可以有多份不同的工作,但是每次只能去做一份 */
64 void doWork1(const QString &parameter) {
65
66 /* 标志位为真 */
67 isCanRun = true;
68
69 /* 死循环 */
70 while (1) {
71 /* 此{}作用是 QMutexLocker 与 lock 的作用范围,获取锁后,
72 * 运行完成后即解锁 */
73 {
74 QMutexLocker locker(&lock);
75 /* 如果标志位不为真 */
76 if (!isCanRun) {
77 /* 跳出循环 */
78 break;
79 }
80 }
81 /* 使用 QThread 里的延时函数,当作一个普通延时 */
82 QThread::sleep(2);
83
84 emit resultReady(parameter + "doWork1 函数");
85 }
86 /* doWork1 运行完成,发送信号 */
87 emit resultReady("打断 doWork1 函数");
88 }
89
90 // void doWork2();...
91
92 public:
93 /* 打断线程(注意此方法不能放在槽函数下) */
94 void stopWork() {
95 qDebug()<<"打断线程"<<endl;
96
97 /* 获取锁后,运行完成后即解锁 */
98 QMutexLocker locker(&lock);
99 isCanRun = false;
100 }
101
102 signals:
103 /* 工人工作函数状态的信号 */
104 void resultReady(const QString &result);
105 };
106 #endif // MAINWINDOW_H

endif // MAINWINDOW_H
第 51~105 行,声明一个 Worker 的类继承 QObject 类,这里是参考 Qt 的 QThread 类的帮助文档的写法。将官方的例子运用到我们的例子里去。
第 62~88 行,我们把耗时的工作都放于槽函数下。工人可以有不同的工作,但是每次只能去做一份。这里不同于继承 QThread 类的线程 run(),继承 QThread 的类只有 run()在新线程里。而继承 QObject 的类,使用 moveToThread()可以把整个继承的 QObject 类移至线程里执行,所以可以有 doWork1(),doWork2…等等耗时的操作,但是这些 耗时的操作都应该作为槽函数,由主线程去调用。
第 67~80 行,进入循环后使用互拆锁判断 isCanRun 变量的状态,为假即跳出 while 循环,直到 doWork1 结束。注意,虽然 doWork1 结束了,但是线程并没有退出(结束)。因为我们把这个类移到线程里了,直到这个类被销毁。或者使用 quit()和 exit()退出线程才真正的结束!

在源文件“mainwindow.cpp”具体代码如下

1 #include "mainwindow.h"
2
3 MainWindow::MainWindow(QWidget *parent)
4 : QMainWindow(parent)
5 {
6 /* 设置显示位置与大小 */
7 this->setGeometry(0, 0, 800, 480);
8 pushButton1 = new QPushButton(this);
9 pushButton2 = new QPushButton(this);
10
11
12 /* 设置按钮的位置大小 */
13 pushButton1->setGeometry(300, 200, 80, 40);
14 pushButton2->setGeometry(400, 200, 80, 40);
15
16 /* 设置两个按钮的文本 */
17 pushButton1->setText("开启线程");
18 pushButton2->setText("打断线程");
19
20 /* 工人类实例化 */
21 worker = new Worker;
22
23 /* 将 worker 类移至线程 workerThread */
24 worker->moveToThread(&workerThread);
25
26 /* 信号槽连接 */
27
28 /* 线程完成销毁对象 */
29 connect(&workerThread, SIGNAL(finished()),
30 worker, SLOT(deleteLater()));
31 connect(&workerThread, SIGNAL(finished()),
32 &workerThread, SLOT(deleteLater()));
33
34 /* 发送开始工作的信号,开始工作 */
35 connect(this, SIGNAL(startWork(QString)),
36 worker, SLOT(doWork1(QString)));
37
38 /* 接收到 worker 发送过来的信号 */
39 connect(worker, SIGNAL(resultReady(QString)),
40 this, SLOT(handleResults(QString)));
41
42 /* 点击按钮开始线程 */
43 connect(pushButton1, SIGNAL(clicked()),
44 this, SLOT(pushButton1Clicked()));
45
46 /* 点击按钮打断线程 */
47 connect(pushButton2, SIGNAL(clicked()),
48 this, SLOT(pushButton2Clicked()));
49 }
50
51 MainWindow::~MainWindow()
52 {
53 /* 打断线程再退出 */
54 worker->stopWork();
55 workerThread.quit();
56
57 /* 阻塞线程 2000ms,判断线程是否结束 */
58 if (workerThread.wait(2000)) {
59 qDebug()<<"线程结束"<<endl;
60 }
61 }
62
63 void MainWindow::pushButton1Clicked()
64 {
65 /* 字符串常量 */
66 const QString str = "正在运行";
67
68 /* 判断线程是否在运行 */
69 if(!workerThread.isRunning()) {
70 /* 开启线程 */
71 workerThread.start();
72 }
73
74 /* 发送正在运行的信号,线程收到信号后执行后返回线程耗时函数 + 此字符串 */
75 emit this->startWork(str);
76 }
77
78 void MainWindow::pushButton2Clicked()
79 {
80 /* 如果线程在运行 */
81 if(workerThread.isRunning()) {
82
83 /* 停止耗时工作,跳出耗时工作的循环 */
84 worker->stopWork();
85 }
86 }
87
88 void MainWindow::handleResults(const QString & results)
89 {
90 /* 打印线程的状态 */
91 qDebug()<<"线程的状态:"<<results<<endl;
92 }

第 20 行,工人类实例化。 继承 QObject 的多线程类不能指定父对象。
第 24 行,工人类实例化后,工人类将自己移至 workerThread 线程里执行。
第 29~32 行,线程结束后,我们需要使用 deleteLater 来销毁 worker 对象和 workerThread对象分配的内存。deleteLater 会确认消息循环中没有这两个线程的对象后销毁。

2. 程序运行效果

点击开启线程按钮后,应用程序输出窗口每隔 2 秒打印“正在运行 doWork1 函数”,当我们点击打断线程按钮后,窗口打印出“打断 doWork1 函数”。点击打断线程,会打断 doWork1函数的循环,doWork1 函数就运行结束了。再点击开启线程,可以再次运行 doWork1 函数。

本例界面简单,仅用了两个按钮和打印语句作为显示部分,但是对初学线程的朋友们友好,因为程序不长。我们可以结合程序的注释,一步步去理解这种线程的写法。重要的是掌握写法,最后才应用到花里胡哨的界面去吧!

Qt----多线程_第5张图片

你可能感兴趣的:(QT5,qt,c++)