简介
我们写的一个应用程序,应用程序跑起来后一般情况下只有一个线程,但是可能也有特殊情况。比如我们前面章节写的例程都跑起来后只有一个线程,就是程序的主线程。线程内的操作都是顺序执行的。恩,顺序执行?试着想一下,我们的程序顺序执行,假设我们的用户界面点击有某个操作是比较耗时的。您会发现界面点击完了,点击界面对应的操作还没有完成,所以就会冻结界面,不能响应,直到操作完成后,才返回到正常的界面里。如果我们的界面是这么设计的话,估计用户得发毛了。
这种情况我们一般是创建一个单独的线程来执行这个比较耗时的操作。比如我们使用摄像头拍照保存照片。恩,很多朋友问,这个不算耗时吧。对的在电脑上使用 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()方法在线程里的。其他类内定义的方法都在主线程内。恩,这样不理解?我们画个图捋一捋。
通过上面的图我们可以看到,主线程内有很多方法在主线程内,但是子线程,只有 run()方法是在子线程里的。run()方法是继承于 QThread 类的方法,用户需要重写这个方法,一般是把耗时的操作写在这个 run()方法里面。
应用实例
本例目的:快速了解继承 QThread 类线程的使用。
本例通过 QThread 类继承线程,然后在 MainWindow 类里使用。通过点击一个按钮开启线程。当线程执行完成时,会发送 resultReady(const QString &s)信号给主线程。流程就这么简单。
在头文件“mainwindow.h”具体代码如下:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QThread>
#include <QDebug>
#include <QPushButton>
/* 使用下面声明的 WorkerThread 线程类 */
class WorkerThread;
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
/* 在 MainWindow 类里声明对象 */
WorkerThread *workerThread;
/* 声明一个按钮,使用此按钮点击后开启线程 */
QPushButton *pushButton;
private slots:
/* 槽函数,用于接收线程发送的信号 */
void handleResults(const QString &result);
/* 点击按钮开启线程 */
void pushButtonClicked();
};
/* 新建一个 WorkerThread 类继承于 QThread */
class WorkerThread : public QThread
{
/* 用到信号槽即需要此宏定义 */
Q_OBJECT
public:
WorkerThread(QWidget *parent = nullptr) {
Q_UNUSED(parent);
}
/* 重写 run 方法,继承 QThread 的类,只有 run 方法是在新的线程里 */
void run() override {
QString result = "线程开启成功";
while(1) {
/* 这里写上比较耗时的操作 */
// ...文件读写,大量遍历,高级算法都算耗时操作
// 延时 2s,把延时 2s 当作耗时操作
sleep(2);
/* 发送结果准备好的信号 */
emit resultReady(result);
}
}
signals:
/* 声明一个信号,译结果准确好的信号 */
void resultReady(const QString &s);
};
#endif // MAINWINDOW_H
声明一个 WorkerThread 的类继承 QThread 类,这里是参考 Qt 的 QThread 类的帮助文档的写法。重写 run()方法,这里很重要。把耗时操作写于此,本例相当于一个继承 QThread类线程模板了。
在源文件“mainwindow.cpp”具体代码如下:
#include "mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
/* 设置位置与大小 */
this->setGeometry(0, 0, 800, 480);
/* 对象实例化 */
pushButton = new QPushButton(this);
workerThread = new WorkerThread(this);
/* 按钮设置大小与文本 */
pushButton->resize(100, 40);
pushButton->setText("开启线程");
/* 信号槽连接 */
connect(workerThread, SIGNAL(resultReady(QString)),
this, SLOT(handleResults(QString)));
connect(pushButton, SIGNAL(clicked()),
this, SLOT(pushButtonClicked()));
}
MainWindow::~MainWindow()
{
/* 进程退出,注意本例 run()方法没写循环,此方法需要有循环才生效 */
workerThread->quit();
/* 阻塞等待 2000ms 检查一次进程是否已经退出 */
if (workerThread->wait(2000)) {
qDebug()<<"线程已经结束!"<<endl;
}
}
void MainWindow::handleResults(const QString &result)
{
/* 打印出线程发送过来的结果 */
qDebug()<<result<<endl;
}
void MainWindow::pushButtonClicked()
{
/* 检查线程是否在运行,如果没有则开始运行 */
if (!workerThread->isRunning())
workerThread->start();
}
线程对象实例化,Qt 使用 C++基本都是对象编程,Qt 线程也不例外。所以我们也是用对象来管理线程的。
在 MainWindow 的析构函数里退出线程,然后判断线程是否退出成功。因为我们这个线程是没有循环操作的,直接点击按钮开启线程后,做了 2s 延时操作后就完成了。所以我们在析构函数里直接退出没有关系。
按钮点击后开启线程,首先我们得判断这个线程是否在运行,如果不在运行我们则开始线程,开始线程用 start()方法,它会调用重写的 run()函数的。
程序运行效果
点击开启线程按钮后,延时 2s后,Qt Creator 的应用程序输出窗口打印出“线程开启成功”。在 2s 内多次点击按钮则不会重复开启线程,因为线程在这 2s 内还在运行。同时我们可以看到点击按钮没卡顿现象。因为这个延时操作是在我们创建的线程里运行的,而 pushButton 是在主线程里的,通过点击按钮控制子线程的运行。
当关闭程序后,子线程将在主线程的析构函数里退出。注意线程使用 wait()方法,这里等待 2s,因为我们开启的线和是延时 2s 就完成了。如果是实际的操作,请根据 CPU 的处理能力,给一个适合的延时,阻塞等待线程完成后,就会自动退出并打印“线程已经结束”。
下一节 继承 QObject 的线程 官方推荐使用的