多线程
继承 QThread 的线程
概述
-
继承 QThread 是创建线程的一个普通方法
-
线程内的方法执行
-
在创建的线程中,只有 run() 方法是在该线程内执行的
-
其他在类内定义的方法,尽管属于子线程类,但它们实际是在主线程内执行的
-
-
主线程的方法执行
- 线程内有很多方法,它们都是在主线程内执行的
-
子线程的方法执行
- 在子线程中,只有 run() 方法是在子线程内执行的
-
重写 run() 方法
-
run() 方法是继承自 QThread 类的方法
-
用户需要重写这个方法
-
一般情况下,耗时的操作会被写在这个 run() 方法里面
-
应用实例
-
基本流程
-
通过 QThread 类继承线程
-
然后在 MainWindow 类里使用
-
通
过点击一个按钮开启线程。当线程执行完成时,会发送 resultReady(const QString &s)信号给主线程
-
-
mainwindow.h
-
1 #ifndef MAINWINDOW_H
2 #define MAINWINDOW_H
3
4 #include
5 #include
6 #include
7 #include
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
-
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 的析构函数里退出线程
-
判断线程是否退出成功
-
由于该线程没有循环操作,因此在按钮点击后,执行了 2 秒延时操作后线程就完成了
-
所以在析构函数里直接退出线程是可以的
-
-
按钮点击后开启线程(第41~46行)
-
按钮点击后开启线程
-
首先判断该线程是否在运行
-
如果线程不在运行,则开始线程
-
开始线程使用 start() 方法,该方法会调用重写的 run() 函数
-
-
程序运行效果
-
点击开启线程按钮后,延时 2s 后,Qt Creator 的应用程序输出窗口打印出“线程开启成功”。
在 2s 内多次点击按钮则不会重复开启线程,因为线程在这 2s 内还在运行。同时我们可以看到
点击按钮没卡顿现象
继承 QObject 的线程
概述
-
继承 QObject 类是创建线程的另一种方法
- 继承 QObject 类更加灵活
-
使用 moveToThread() 方法
- 通过 QObject::moveToThread() 方法,可以将一个 QObject 的类转移到一个线程里执行
-
创建继承 QObject 的类
- 首先,编写一个类继承 QObject
-
将类移到 QThread 线程执行
- 使用 QObject::moveToThread() 方法将该类移到一个 QThread 线程里执行
-
主线程与 QThread 线程的通信
- 可以通过主线程发送信号,来调用 QThread 线程的方法
-
QThread 线程内方法的执行
- 例如,使用信号调用的 fun4(), fun5() 等方法,这些方法都是在 QThread 线程里执行的
应用实例
-
基本思路
-
通过 QObject 类继承线程,在 MainWindow 类里使用
- 通过继承 QObject 类创建线程,并在 MainWindow 类里使用
-
开启和关闭线程的操作
-
通过点击一个按钮来开启线程
-
通过另一个按钮点击关闭线程
-
-
使用加锁操作来安全终止线程
-
通过加锁操作来确保线程的安全终止
-
可以使用 QMutexLocker 安全地使用 QMutex,避免忘记解锁的情况
-
-
为什么需要加锁来终止线程
-
quit() 和 exit() 方法不会立即终止线程
-
terminate() 方法可以立即终止线程,但存在非常不安全的因素,Qt 官方文档不推荐使用。
-
-
使用 bool 变量来终止线程
-
可以添加一个 bool 变量,通过主线程修改该变量来终止线程
-
这种方法可能会引起访问冲突,因此需要加锁
-
-
加锁在多任务情况下的重要性
-
在有多个任务(如 doWork1(), doWork2())时,加锁可以防止访问冲突
-
加锁会消耗一定的性能,增加执行时间
-
-
-
mainwindow.h
-
1 #ifndef MAINWINDOW_H
2 #define MAINWINDOW_H
3
4 #include
5 #include
6 #include
7 #include
8 #include
9 #include
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 ¶meter) {
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 -
声明 Worker 类继承 QObject 类(第51~105行)
-
声明一个名为 Worker 的类,继承自 QObject 类
-
这里参考了 Qt 的 QThread 类的帮助文档,将官方例子运用到我们的例子中
-
-
耗时工作放于槽函数中(第62~88行)
-
将耗时的工作都放在槽函数下
-
虽然工人可以有不同的工作,但每次只能做一份
-
这与继承 QThread 类的线程不同,继承 QThread 类的线程只有 run() 在新线程里执行
-
继承 QObject 的类使用 moveToThread() 可以将整个继承的 QObject 类移至线程里执行
-
因此,可以有多个耗时操作方法如 doWork1(), doWork2() 等,但这些操作都应该作为槽函数,由主线程调用
-
-
循环中的互斥锁判断(第67~80行)
-
在循环中使用互拆锁判断 isCanRun 变量的状态
-
如果 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行)
-
在第20行,实例化工人类
-
注意,继承 QObject 的多线程类不能指定父对象
-
-
移至 workerThread 线程执行(第24行)
- 工人类实例化后,将工人类对象移至 workerThread 线程里执行
-
线程结束后的内存管理(第29~32行)
-
在线程结束后,需要使用 deleteLater 来销毁 worker 对象和 workerThread 对象分配的内存
-
deleteLater 会在消息循环中确认没有这两个对象的事件后,再进行销毁
-
-
程序运行效果
-
点击开启线程按钮后,应用程序输出窗口每隔 2 秒打印“正在运行 doWork1 函数”,当我们点击打断线程按钮后,窗口打印出“打断 doWork1 函数”。 点击打断线程,会打断 doWork1函数的循环,doWork1 函数就运行结束了。再点击开启线程,可以再次运行 doWork1 函数