|
本帖最后由 正点原子 于 2021-8-11 12:27 编辑
1)实验平台:正点原子i.MX6ULL Linux阿尔法开发板
2) 章节摘自【正点原子】《I.MX6U嵌入式Qt开发指南》
3)购买链接:https://item.taobao.com/item.htm?&id=603672744434
4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/arm-linux/zdyz-i.mx6ull.html
5)正点原子官方B站:https://space.bilibili.com/394620890
6)正点原子Linux技术交流群:1027879335
第十章 多线程
我们写的一个应用程序,应用程序跑起来后一般情况下只有一个线程,但是可能也有特殊情况。比如我们前面章节写的例程都跑起来后只有一个线程,就是程序的主线程。线程内的操作都是顺序执行的。恩,顺序执行?试着想一下,我们的程序顺序执行,假设我们的用户界面点击有某个操作是比较耗时的。您会发现界面点击完了,点击界面对应的操作还没有完成,所以就会冻结界面,不能响应,直到操作完成后,才返回到正常的界面里。如果我们的界面是这么设计的话,估计用户得发毛了。
这种情况我们一般是创建一个单独的线程来执行这个比较耗时的操作。比如我们使用摄像头拍照保存照片。恩,很多朋友问,这个不算耗时吧。对的在电脑上使用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线程的创建,理解线程。
10.1 继承QThread的线程
在第十章的章节开头说过了,继承QThread是创建线程的一个普通方法。其中创建的线程只有run()方法在线程里的。其他类内定义的方法都在主线程内。恩,这样不理解?我们画个图捋一捋。
通过上面的图我们可以看到,主线程内有很多方法在主线程内,但是子线程,只有run()方法是在子线程里的。run()方法是继承于QThread类的方法,用户需要重写这个方法,一般是把耗时的操作写在这个run()方法里面。
10.1.1 应用实例
本例目的:快速了解继承QThread类线程的使用。
例05_qthread_example1,继承QThread类的线程(难度:一般)。项目路径为Qt/2/05_qthread_example1。本例通过QThread类继承线程,然后在MainWindow类里使用。通过点击一个按钮开启线程。当线程执行完成时,会发送resultReady(const QString &s)信号给主线程。流程就这么简单。
在头文件“mainwindow.h”具体代码如下。
mainwindow.h编程后的代码
- /******************************************************************
- Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
- * @projectName 05_qthread_example1
- * @brief mainwindow.h
- * @author Deng Zhimao
- * @email <a href="mailto:1252699831@qq.com">1252699831@qq.com</a>
- * @net <a href="www.openedv.com" target="_blank">www.openedv.com</a>
- * @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”具体代码如下。
mainwindow.cpp编程后的代码
- /******************************************************************
- Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
- * @projectName 05_qthread_example1
- * @brief mainwindow.cpp
- * @author Deng Zhimao
- * @email <a href="mailto:1252699831@qq.com">1252699831@qq.com</a>
- * @net <a href="www.openedv.com" target="_blank">www.openedv.com</a>
- * @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()函数的。
10.1.2 程序运行效果
点击开启线程按钮后,延时2s后,Qt Creator的应用程序输出窗口打印出“线程开启成功”。在2s内多次点击按钮则不会重复开启线程,因为线程在这2s内还在运行。同时我们可以看到点击按钮没卡顿现象。因为这个延时操作是在我们创建的线程里运行的,而pushButton是在主线程里的,通过点击按钮控制子线程的运行。
当关闭程序后,子线程将在主线程的析构函数里退出。注意线程使用wait()方法,这里等待2s,因为我们开启的线和是延时2s就完成了。如果是实际的操作,请根据CPU的处理能力,给一个适合的延时,阻塞等待线程完成后,就会自动退出并打印“线程已经结束”。
10.2 继承QObject的线程
在第10章章节开头已经说过,继承QThread类是创建线程的一种方法,另一种就是继承QObject类。继承QObject类更加灵活。它通过QObject::moveToThread()方法,将一个QObeject的类转移到一个线程里执行。恩,不理解的话,我们下面也画个图捋一下。
通过上面的图不难理解,首先我们写一个类继承QObject,通过QObject::moveToThread()方法将它移到一个QThread线程里执行。那么可以通过主线程发送信号去调用QThread线程的方法如上图的fun4(),fun5()等等。这些方法都是在QThread线程里执行的。
10.2.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官方写的,看似简单,但是流程大家可能不是很明白,所以画个了大体的流程图,给大伙瞧瞧。
在头文件“mainwindow.h”具体代码如下。
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”具体代码如下。
mainwindow.cpp编程后的代码
- /******************************************************************
- Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
- * @projectName 06_qthread_example2
- * @brief mainwindow.cpp
- * @author Deng Zhimao
- * @email <a href="mailto:1252699831@qq.com">1252699831@qq.com</a>
- * @net <a href="www.openedv.com" target="_blank">www.openedv.com</a>
- * @date 2021-04-08
- *******************************************************************/
- 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会确认消息循环中没有这两个线程的对象后销毁。
10.2.2 程序运行效果
点击开启线程按钮后,应用程序输出窗口每隔2秒打印“正在运行doWork1函数”,当我们点击打断线程按钮后,窗口打印出“打断doWork1函数”。点击打断线程,会打断doWork1函数的循环,doWork1函数就运行结束了。再点击开启线程,可以再次运行doWork1函数。本例界面简单,仅用了两个按钮和打印语句作为显示部分,但是对初学线程的朋友们友好,因为程序不长。我们可以结合程序的注释,一步步去理解这种线程的写法。重要的是掌握写法,最后才应用到花里胡哨的界面去吧!
|
阿莫论坛20周年了!感谢大家的支持与爱护!!
该献的血还是要献的。你不献他不献。难道让我去献? --- 出自坛友:lovejp1981
|