Skip to content

Latest commit

 

History

History
530 lines (391 loc) · 18.4 KB

File metadata and controls

530 lines (391 loc) · 18.4 KB

七、线程基础——异步编程

本章将涵盖以下食谱:

  • 使用线程
  • 对象和线程
  • 数据保护和线程间共享数据
  • 使用不可运行的流程

介绍

大多数现代软件并行运行其进程,并将任务卸载到不同的线程,以利用现代 CPU 多核架构。这样,软件可以通过同时运行多个进程来提高效率,而不会影响性能。在本章中,我们将学习如何利用线程来提高我们的 Qt 5 应用的性能和效率。

技术要求

本章的技术要求包括安装 Qt 5.11.2 MinGW 32 位、Qt Creator 4.8.2 和 Windows 10。

本章使用的所有代码都可以从下面的 GitHub 链接下载:https://GitHub . com/PacktPublishing/Qt5-CPP-GUI-编程-cook book-第二版/树/master/Chapter07

查看以下视频,查看正在运行的代码:http://bit.ly/2TnFJUU

使用线程

Qt 5 提供了多种创建和使用线程的方法。您可以在高级方法和低级方法之间进行选择。高级方法更容易上手,但受限于你能用它们做什么。另一方面,低级方法更灵活,但对初学者不友好。在本食谱中,我们将学习如何使用其中一种高级方法轻松创建多线程 Qt 5 应用。

怎么做…

让我们按照以下步骤学习如何创建多线程应用:

  1. 创建一个 Qt 小部件应用并打开main.cpp。然后,在文件顶部添加以下标题:
#include <QFuture>
#include <QtConcurrent/QtConcurrent>
#include <QFutureWatcher>
#include <QThread>
#include <QDebug>
  1. 然后,在main()函数之前创建一个名为printText()的函数:
void printText(QString text, int count) {
    for (int i = 0; i < count; ++ i)
        qDebug() << text << QThread::currentThreadId();
    qDebug() << text << "Done";
}
  1. 之后,在main()功能中添加以下代码:
int main(int argc, char *argv[]) {
    QApplication a(argc, argv);
    MainWindow w;
    w.show();
 printText("A", 100);
 printText("B", 100);
    return a.exec();
}
  1. 如果您现在构建并运行程序,您应该会看到在打印B之前先打印A。请注意,它们的线程标识都是相同的。这是因为我们正在主线程中运行printText()函数:
...
"A" 0x2b82c
"A" 0x2b82c
"A" 0x2b82c
"A" Done
...
"B" 0x2b82c
"B" 0x2b82c
"B" 0x2b82c
"B" Done
  1. 为了将它们分成不同的线程,让我们使用 Qt 5 提供的名为QFuture的高级类。让我们注释掉main()中的两个printText()函数,并改用下面的代码:
QFuture<void> f1 = QtConcurrent::run(printText, QString("A"), 100);
QFuture<void> f2 = QtConcurrent::run(printText, QString("B"), 100);
QFuture<void> f3 = QtConcurrent::run(printText, QString("C"), 100);
f1.waitForFinished();
f2.waitForFinished();
f3.waitForFinished();
  1. 如果您再次构建并运行程序,您应该会在调试窗口上看到类似这样的内容,这意味着三个printText()函数现在并行运行:
...
"A" 0x271ec
"C" 0x26808
"B" 0x27a40
"A" 0x271ec
"C" Done
"B" 0x27a40
"A" Done
"B" Done
  1. 我们也可以使用QFutureWatcher通过信号和时隙机制通知一个QObject类:
QFuture<void> f1 = QtConcurrent::run(printText, QString("A"), 100);
QFuture<void> f2 = QtConcurrent::run(printText, QString("B"), 100);
QFuture<void> f3 = QtConcurrent::run(printText, QString("C"), 100);

QFutureWatcher<void> futureWatcher;
QObject::connect(&futureWatcher, QFutureWatcher<void>::finished, &w, MainWindow::mySlot);
futureWatcher.setFuture(f1);

f1.waitForFinished();
f2.waitForFinished();
f3.waitForFinished();
  1. 之后,打开mainwindow.h并声明槽功能:
public slots:
    void mySlot();
  1. mySlot()功能在mainwindow.cpp中是这样的:
void MainWindow::mySlot() {
    qDebug() << "Done!" << QThread::currentThreadId();
}
  1. 如果您再次构建并运行该程序,这一次,您将看到如下结果:
...
"A" 0x271ec
"C" 0x26808
"B" 0x27a40
"A" 0x271ec
"C" Done
"B" 0x27a40
"A" Done
"B" Done
Done! 0x27ac0
  1. 即使QFutureWatcher链接到f1,但是Done!消息只有在所有线程执行完毕后才会被打印出来。这是因为mySlot()正在主线程中运行,调试窗口中Done!消息旁边显示的线程标识证明了这一点。

它是如何工作的...

默认情况下,在任何 Qt 5 应用中都有一个主线程(也称为图形用户界面线程)。您创建的其他线程称为工作线程

与 GUI 相关的类,比如QWidgetsQPixmap,只能存在于主线程中,所以在处理这些类的时候一定要格外小心。

QFuture是处理异步计算的高级类。

我们使用QFutureWatcher类让QFuture与信号和插槽交互。您甚至可以使用它在进度条上显示操作进度。

对象和线程

接下来,我们想探索一些其他的方法,以便在 Qt 5 应用中使用线程。Qt 5 提供了一个名为QThread的类,它可以让你更好地控制如何创建和执行一个线程。一个QThread对象通过调用run()函数开始在线程中执行其事件循环。在这个例子中,我们将通过QThread学习如何让QObject异步协同工作。

怎么做…

让我们从执行以下步骤开始:

  1. 创建一个新的 Qt 小部件应用项目。然后,转到文件|新文件或项目...并创建一个 C++ 类:

  1. 之后命名新类MyWorker,使其继承自QObject类。不要忘记默认包括QObject类:

  1. 一旦创建了MyWorker类,打开myworker.h并在顶部添加以下标题:
#include <QObject>
#include <QDebug>
  1. 之后,将以下信号和插槽功能也添加到文件中:
signals:
    void showResults(int res);
    void doneProcess();

public slots:
    void process();
  1. 接下来,打开myworker.cpp并执行process()功能:
void MyWorker::process() {
    int result = 0;
    for (int i = 0; i < 2000000000; ++ i) {
        result += 1;
    }
    emit showResults(result);
    emit doneProcess();
}
  1. 之后,打开mainwindow.h并在顶部添加以下标题:
#include <QDebug>
#include <QThread>
#include "myworker.h"
  1. 然后,声明一个槽函数,如下面的代码所示:
public slots:
    void handleResults(int res);
  1. 完成后,打开mainwindow.cpp并执行handResults()功能:
void MainWindow::handleResults(int res) {
    qDebug() << "Handle results" << res;
}
  1. 最后,我们将向MainWindow类的类构造函数添加以下代码:
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow){
    ui->setupUi(this);
    QThread* workerThread = new QThread;
    MyWorker *workerObject = new MyWorker;
    workerObject->moveToThread(workerThread);
    connect(workerThread, &QThread::started, workerObject, &MyWorker::process);
    connect(workerObject, &MyWorker::doneProcess, workerThread, &QThread::quit);
    connect(workerObject, &MyWorker::doneProcess, workerObject, &MyWorker::deleteLater);
    connect(workerObject, &MyWorker::showResults, this, &MainWindow::handleResults);
    connect(workerThread, &QThread::finished, workerObject, &MyWorker::deleteLater);
    workerThread->start();
}
  1. 立即构建并运行程序。您应该看到,在调试窗口上打印一行消息之前,主窗口会弹出,几秒钟内什么也不做:
Final result: 2000000000
  1. 结果是在一个单独的线程中计算的,这就是为什么主窗口可以平滑显示,甚至可以在计算过程中用鼠标移动。为了查看在主线程上运行计算时的区别,让我们注释掉一些代码,直接调用process()函数:
//QThread* workerThread = new QThread;
MyWorker *workerObject = new MyWorker;
//workerObject->moveToThread(workerThread);
//connect(workerThread, &QThread::started, workerObject, &MyWorker::process);
//connect(workerObject, &MyWorker::doneProcess, workerThread, &QThread::quit);
connect(workerObject, &MyWorker::doneProcess, workerObject, &MyWorker::deleteLater);
connect(workerObject, &MyWorker::showResults, this, &MainWindow::handleResults);
//connect(workerThread, &QThread::finished, workerObject, &MyWorker::deleteLater);
//workerThread->start();
workerObject->process();
  1. 立即构建并运行项目。这一次,主窗口只会在计算完成后出现在屏幕上。这是因为计算阻塞了主线程(或图形用户界面线程)并阻止了主窗口的显示。

它是如何工作的...

QThread是异步运行进程的替代方法,除了使用QFuture类。与QFuture相比,它给了我们更多的控制权,我们将在下面的食谱中演示。

请注意,移动到工作线程的QObject类不能有任何父类,因为 Qt 的设计方式是整个对象树必须存在于同一个线程中。因此,当你调用moveToThread()时,一个QObject类的所有孩子也会被移动到工作线程。

如果您希望您的工作线程与主线程通信,请使用信号和插槽机制。

我们使用QThread类提供的started信号通知我们的工作对象开始计算,因为工作线程已经创建。

然后,当计算完成后,我们发出showResultdoneProcess信号通知线程退出,同时将最终结果传递给主线程进行打印。

最后,我们还使用信号和槽机制来安全地删除工作线程和工作对象。

数据保护和线程间共享数据

即使多线程使进程异步运行,也会有线程必须停止并等待其他线程的时候。这通常发生在两个线程同时修改同一个变量的时候。强制线程相互等待以保护共享资源(如数据)是很常见的。Qt 5 还提供了低级方法和高级机制来同步线程。

怎么做…

我们将继续使用上一个示例项目中的代码,因为我们已经建立了一个多线程工作程序:

  1. 打开myworker.h,添加如下表头:
#include <QObject>
#include <QDebug>
#include <QMutex>
  1. 然后,我们将添加两个新变量,并对类构造函数进行一些更改:
public:
    explicit MyWorker(QMutex *mutex);
 int* myInputNumber;
 QMutex* myMutex;

signals:
    void showResults(int res);
    void doneProcess();
  1. 之后,打开myworker.cpp,将类构造函数改为如下代码。我们不再需要父输入,因为对象没有父项:
MyWorker::MyWorker(QMutex *mutex) {
 myMutex = mutex;
}
  1. 我们还会将process()功能更改为如下所示:
void MyWorker::process() {
    myMutex->lock();
    for (int i = 1; i < 100000; ++ i){
        *myInputNumber += i * i + 2 * i + 3 * i;
    }
    myMutex->unlock();
    emit showResults(*myInputNumber);
    emit doneProcess();
}
  1. 完成后,打开mainwindow.cpp并对代码进行一些更改:
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) {
    ui->setupUi(this);

    int myNumber = 5;
    QMutex* newMutex = new QMutex;

    QThread* workerThread = new QThread;
    QThread* workerThread2 = new QThread;
    QThread* workerThread3 = new QThread;
    MyWorker *workerObject = new MyWorker(newMutex);
    MyWorker *workerObject2 = new MyWorker(newMutex);
    MyWorker *workerObject3 = new MyWorker(newMutex);
  1. 之后,我们将工作对象的myInputNumber变量设置为myNumber。请注意,我们引用的是它的指针而不是值:
workerObject->myInputNumber = &myNumber;
workerObject->moveToThread(workerThread);
connect(workerThread, &QThread::started, workerObject, &MyWorker::process);
connect(workerObject, &MyWorker::doneProcess, workerThread, &QThread::quit);
connect(workerObject, &MyWorker::doneProcess, workerObject, &MyWorker::deleteLater);
connect(workerObject, &MyWorker::showResults, this, &MainWindow::handleResults);
connect(workerThread, &QThread::finished, workerObject, &MyWorker::deleteLater);
  1. 重复上一步两次,设置workerObject2workerThread2workerObject3workerThread3:
workerObject2->myInputNumber = &myNumber;
workerObject2->moveToThread(workerThread2);
connect(workerThread2, &QThread::started, workerObject2, &MyWorker::process);
connect(workerObject2, &MyWorker::doneProcess, workerThread2, &QThread::quit);
connect(workerObject2, &MyWorker::doneProcess, workerObject2, &MyWorker::deleteLater);
connect(workerObject2, &MyWorker::showResults, this, &MainWindow::handleResults);
connect(workerThread2, &QThread::finished, workerObject2, &MyWorker::deleteLater);

workerObject3->myInputNumber = &myNumber;
workerObject3->moveToThread(workerThread3);
connect(workerThread3, &QThread::started, workerObject3, &MyWorker::process);
connect(workerObject3, &MyWorker::doneProcess, workerThread3, &QThread::quit);
connect(workerObject3, &MyWorker::doneProcess, workerObject3, &MyWorker::deleteLater);
connect(workerObject3, &MyWorker::showResults, this, &MainWindow::handleResults);
connect(workerThread3, &QThread::finished, workerObject3, &MyWorker::deleteLater);
  1. 最后,我们将通过调用start()开始运行这些线程:
workerThread->start();
workerThread2->start();
workerThread3->start();
  1. 如果您现在构建并运行该程序,无论运行多少次,您都应该看到一致的结果:
Final result: -553579035
Final result: -1107158075
Final result: -1660737115
  1. 我们每次运行程序都会得到结果,因为互斥锁确保只有一个线程能够修改数据,而其他线程等待数据完成。要了解没有互斥锁的区别,让我们注释一下代码:
void MyWorker::process() {
 //myMutex->lock();

    for (int i = 1; i < 100000; ++ i) {
        *myInputNumber += i * i + 2 * i + 3 * i;
    }

 //myMutex->unlock();

    emit showResults(*myInputNumber);
    emit doneProcess();
}
  1. 再次构建并运行程序。这一次,当你运行程序时,你会得到一个非常不同的结果。例如,我在三种情况下运行它时获得了以下结果:
1st time:
Final result: -589341102
Final result: 403417142
Final result: -978935318

2nd time:
Final result: 699389030
Final result: -175723048
Final result: 1293365532

3rd time:
Final result: 1072831160
Final result: 472989964
Final result: -534842088
  1. 发生这种情况是因为myNumber数据被所有线程以随机顺序同时操纵,这是由于并行计算的特性。通过锁定互斥体,我们确保数据只能由单个线程修改,从而消除了这个问题。

它是如何工作的...

Qt 5 提供了QMutexQReadWriteLock两个类,用于多个线程访问和修改同一数据时的数据保护。我们在前面的例子中只使用了QMutex,但是这两个类本质上非常相似。唯一的区别是QReadWriteLock允许其他线程在写入数据的同时读取数据。与QMutex不同,它将读和写状态分开,但一次只能出现一个状态(要么锁定为读,要么锁定为写),而不是两者都出现。对于复杂的函数和语句,使用高级的QMutexLocker类代替QMutex来简化代码,更容易调试。

这种方法的缺点是,当数据被单个线程修改时,所有其他线程都将无所事事。最好不要与多个线程共享数据,除非没有其他方法,因为这将停止其他线程并挫败并行计算的目标。

使用不可运行的流程

在本食谱中,我们将学习如何使用另一种高级方法轻松创建多线程 Qt 5 应用。我们将使用本食谱中的QRunnableQThreadPool课程。

怎么做…

让我们从执行以下步骤开始:

  1. 创建一个新的 Qt Widgets Application 项目,并创建一个名为MyProcess的新 C++ 类,该类继承了QRunnable类。
  2. 接下来,打开myprocess.h并添加以下标题:
#include <QRunnable>
#include <QDebug>
  1. 然后,声明run()功能,如下所示:
class MyProcess : public QRunnable {
public:
    MyProcess();
 void run();
};
  1. 之后,打开myprocess.cpp并定义run()功能:
void MyProcess::run() {
    int myNumber = 0;
    for (int i = 0; i < 100000000; ++ i) {
        myNumber += i;
    }
    qDebug() << myNumber;
}
  1. 完成后,将以下标题添加到mainwindow.h:
#include <QMainWindow>
#include <QThreadPool>
#include "myprocess.h"
  1. 之后,我们将通过添加以下代码来实现类构造函数:
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) {
  ui->setupUi(this);

  MyProcess* process = new MyProcess;
  MyProcess* process2 = new MyProcess;
  MyProcess* process3 = new MyProcess;
  MyProcess* process4 = new MyProcess;

  QThreadPool::globalInstance()->start(process);
  QThreadPool::globalInstance()->start(process2);
  QThreadPool::globalInstance()->start(process3);
  QThreadPool::globalInstance()->start(process4);

  qDebug() << QThreadPool::globalInstance()->activeThreadCount();
}
  1. 现在,构建并运行该项目。您应该看到这些进程正在不同的线程中成功运行,其中活动线程数为 4。
  2. QThreadPool类在执行完最后一个进程后会自动停用线程。让我们尝试通过暂停程序三秒钟并再次打印出活动线程数来证明这一点:
qDebug() << QThreadPool::globalInstance()->activeThreadCount();
this->thread()->sleep(3);
qDebug() << QThreadPool::globalInstance()->activeThreadCount();
  1. 再次构建并运行程序。这一次,您应该看到活动线程数是四,然后,三秒钟后,活动线程数变成零。这是因为所有的流程都已经执行了。

它是如何工作的...

QRunnable类与管理线程集合的QThreadPool类协同工作。QThreadPool类自动管理和回收单个QThreads 对象,以避免过于频繁地创建和销毁线程,这有助于降低计算成本。

要使用QThreadPool,必须对QRunnable对象进行子类化,并实现名为run()的虚拟函数。默认情况下,当最后一个线程退出run功能时,QThreadPool会自动删除QRunnable对象。您可以通过调用setAutoDelete()autoDelete变量更改为false来更改此行为。

默认情况下,超过 30 秒未使用的线程将过期。您可以在线程运行之前通过调用setExpiryTimeout()来更改该持续时间。否则,它不会对超时设置产生任何影响。

也可以通过调用setMaxThreadCount()设置可以使用的最大线程数。要获取当前活动线程的总数,只需调用activeThreadCount()