Qt学习:Qt 进程和线程之四,线程实际应用

痛定思痛。 2023-10-14 15:21 93阅读 0赞

为了让程序尽快响应用户操作,在开发应用程序时经常会使用到线程。对于耗时操作如果不使用线程,UI 界面将会长时间处于停滞状态,这种情况是用户非常不愿意看到的,我们可以用线程来解决这个问题。

大多数情况下,多线程耗时操作会与 UI 进行交互,比如:显示进度、加载等待。。。让用户明确知道目前的状态,并对结果有一个直观的预期,甚至有趣巧妙的设计,能让用户爱上等待,把等待看成一件很美好的事。

一、多线程操作 UI 界面的示例

下面,是一个使用多线程操作 UI 界面的示例 - 更新进度条,采用子类化 QThread 的方式。与此同时,分享在此过程中有可能遇到的问题及解决方法。

首先创建 QtGui 应用,工程名称为 “myThreadBar”,类名选择 “QMainWindow”,其他选项保持默认即可。再添加一个名称为 WorkerThread 的头文件,定义一个 WorkerThread 类,让其继承自 QThread,并重写 run () 函数,修改 workerthread.h 文件如下:

  1. #ifndef WORKERTHREAD_H
  2. #define WORKERTHREAD_H
  3. #include <QThread>
  4. #include <QDebug>
  5. class WorkerThread : public QThread
  6. {
  7. Q_OBJECT
  8. public:
  9. explicit WorkerThread(QObject *parent = 0)
  10. : QThread(parent)
  11. {
  12. qDebug() << "Worker Thread : " << QThread::currentThreadId();
  13. }
  14. protected:
  15. virtual void run() Q_DECL_OVERRIDE
  16. {
  17. qDebug() << "Worker Run Thread : " << QThread::currentThreadId();
  18. int nValue = 0;
  19. while (nValue < 100)
  20. {
  21. // 休眠50毫秒
  22. msleep(50);
  23. ++nValue;
  24. // 准备更新
  25. emit resultReady(nValue);
  26. }
  27. }
  28. signals:
  29. void resultReady(int value);
  30. };
  31. #endif // WORKERTHREAD_H

通过在 run () 函数中调用 msleep (50),线程会每隔 50 毫秒让当前的进度值加 1,然后发射一个 resultReady () 信号,其余时间什么都不做。在这段空闲时间,线程不占用任何的系统资源。当休眠时间结束,线程就会获得 CPU 时钟,将继续执行它的指令。在 mainwindow.ui 上添加一个按钮和进度条部件,然后 mainwindow.h 修改如下:

  1. #ifndef MAINWINDOW_H
  2. #define MAINWINDOW_H
  3. #include <QMainWindow>
  4. #include "workerthread.h"
  5. namespace Ui {
  6. class MainWindow;
  7. }
  8. class MainWindow : public QMainWindow
  9. {
  10. Q_OBJECT
  11. public:
  12. explicit MainWindow(QWidget *parent = nullptr);
  13. ~MainWindow();
  14. private slots:
  15. // 更新进度
  16. void handleResults(int value);
  17. // 开启线程
  18. void startThread();
  19. private:
  20. Ui::MainWindow *ui;
  21. WorkerThread m_workerThread;
  22. };
  23. #endif // MAINWINDOW_H

然后 mainwindow.cpp 修改如下:

  1. #include "mainwindow.h"
  2. #include "ui_mainwindow.h"
  3. MainWindow::MainWindow(QWidget *parent) :
  4. QMainWindow(parent),
  5. ui(new Ui::MainWindow)
  6. {
  7. ui->setupUi(this);
  8. qDebug() << "Main Thread : " << QThread::currentThreadId();
  9. // 连接信号槽
  10. this->connect(ui->pushButton, SIGNAL(clicked(bool)), this, SLOT(startThread()));
  11. }
  12. MainWindow::~MainWindow()
  13. {
  14. delete ui;
  15. }
  16. void MainWindow::handleResults(int value)
  17. {
  18. qDebug() << "Handle Thread : " << QThread::currentThreadId();
  19. ui->progressBar->setValue(value);
  20. }
  21. void MainWindow::startThread()
  22. {
  23. WorkerThread *workerThread = new WorkerThread(this);
  24. this->connect(workerThread, SIGNAL(resultReady(int)), this, SLOT(handleResults(int)));
  25. // 线程结束后,自动销毁
  26. this->connect(workerThread, SIGNAL(finished()), workerThread, SLOT(deleteLater()));
  27. workerThread->start();
  28. }

由于信号与槽连接类型默认为 “Qt::AutoConnection”,在这里相当于 “Qt::QueuedConnection”。也就是说,槽函数在接收者的线程(主线程)中执行。

执行程序,“应用程序输出” 窗口输出如下:

  1. Main Thread : 0x3140
  2. Worker Thread : 0x3140
  3. Worker Run Thread : 0x2588
  4. Handle Thread : 0x3140

显然,UI 界面、Worker 构造函数、槽函数处于同一线程(主线程),而 run () 函数处于另一线程(次线程)。

回到顶部

二、避免多次 connect

当多次点击 “开始” 按钮的时候,就会多次 connect (),从而启动多个线程,同时更新进度条。为了避免这个问题,我们先在 mainwindow.h 上添加私有成员变量 “WorkerThread m_workerThread;”,然后修改 mainwindow.cpp 如下:

  1. #include "mainwindow.h"
  2. #include "ui_mainwindow.h"
  3. MainWindow::MainWindow(QWidget *parent) :
  4. QMainWindow(parent),
  5. ui(new Ui::MainWindow)
  6. {
  7. ui->setupUi(this);
  8. // 连接信号槽
  9. this->connect(ui->pushButton, SIGNAL(clicked(bool)), this, SLOT(startThread()));
  10. this->connect(&m_workerThread, SIGNAL(resultReady(int)), this, SLOT(handleResults(int)));
  11. }
  12. MainWindow::~MainWindow()
  13. {
  14. delete ui;
  15. }
  16. void MainWindow::handleResults(int value)
  17. {
  18. qDebug() << "Handle Thread : " << QThread::currentThreadId();
  19. ui->progressBar->setValue(value);
  20. }
  21. void MainWindow::startThread()
  22. {
  23. if (!m_workerThread.isRunning())
  24. m_workerThread.start();
  25. }

不再在 startThread () 函数内创建 WorkerThread 对象指针,而是定义私有成员变量,再将 connect 添加在构造函数中,保证了信号槽的正常连接。在线程 start () 之前,可以使用 isFinished () 和 isRunning () 来查询线程的状态,判断线程是否正在运行,以确保线程的正常启动。

三、优雅地结束线程的两种方法

如果一个线程运行完成,就会结束。可很多情况并非这么简单,由于某种特殊原因,当线程还未执行完时,我们就想中止它。

不恰当的中止往往会引起一些未知错误。比如:当关闭主界面的时候,很有可能次线程正在运行,这时,就会出现如下提示:

  1. QThread: Destroyed while thread is still running

这是因为次线程还在运行,就结束了 UI 主线程,导致事件循环结束。这个问题在使用线程的过程中经常遇到,尤其是耗时操作。大多数情况下,当程序退出时,次线程也许会正常退出。这时,虽然抱着侥幸心理,但隐患依然存在,也许在极少数情况下,就会出现 Crash。

所以,我们应该采取合理的措施来优雅地结束线程,一般思路:

  1. 发起线程退出操作,调用 quit () 或 exit ()。
  2. 等待线程完全停止,删除创建在堆上的对象。
  3. 适当的使用 wait ()(用于等待线程的退出)和合理的算法。

方法一

这种方式是 Qt4.x 中比较常用的,主要是利用 “QMutex 互斥锁 + bool 成员变量” 的方式来保证共享数据的安全性。在 workerthread.h 上继续添加互斥锁、析构函数和 stop () 函数,修改如下:

  1. #ifndef WORKERTHREAD_H
  2. #define WORKERTHREAD_H
  3. #include <QThread>
  4. #include <QMutexLocker>
  5. #include <QDebug>
  6. class WorkerThread : public QThread
  7. {
  8. Q_OBJECT
  9. public:
  10. explicit WorkerThread(QObject *parent = 0)
  11. : QThread(parent),
  12. m_bStopped(false)
  13. {
  14. qDebug() << "Worker Thread : " << QThread::currentThreadId();
  15. }
  16. ~WorkerThread()
  17. {
  18. stop();
  19. quit();
  20. wait();
  21. }
  22. void stop()
  23. {
  24. qDebug() << "Worker Stop Thread : " << QThread::currentThreadId();
  25. QMutexLocker locker(&m_mutex);
  26. m_bStopped = true;
  27. }
  28. protected:
  29. virtual void run() Q_DECL_OVERRIDE
  30. {
  31. qDebug() << "Worker Run Thread : " << QThread::currentThreadId();
  32. int nValue = 0;
  33. while (nValue < 100)
  34. {
  35. // 休眠50毫秒
  36. msleep(50);
  37. ++nValue;
  38. // 准备更新
  39. emit resultReady(nValue);
  40. // 检测是否停止
  41. {
  42. QMutexLocker locker(&m_mutex);
  43. if (m_bStopped)
  44. break;
  45. }
  46. // locker超出范围并释放互斥锁
  47. }
  48. }
  49. signals:
  50. void resultReady(int value);
  51. private:
  52. bool m_bStopped;
  53. QMutex m_mutex;
  54. };
  55. #endif // WORKERTHREAD_H

当主窗口被关闭,其 “子对象” WorkerThread 也会析构调用 stop () 函数,使 m_bStopped 变为 true,则 break 跳出循环结束 run () 函数,结束进程。当主线程调用 stop () 更新 m_bStopped 的时候,run () 函数也极有可能正在访问它(这时,他们处于不同的线程),所以存在资源竞争,因此需要加锁,保证共享数据的安全性。

  1. 为什么要加锁?
  2. 很简单,是为了共享数据段操作的互斥。避免形成资源竞争的情况(多个线程有可能访问同一共享资源的情况)。

方法二

Qt5 以后,可以使用 requestInterruption ()、isInterruptionRequested () 这两个函数,使用很方便,修改 workerthread.h 文件如下:

  1. #ifndef WORKERTHREAD_H
  2. #define WORKERTHREAD_H
  3. #include <QThread>
  4. #include <QMutexLocker>
  5. #include <QDebug>
  6. class WorkerThread : public QThread
  7. {
  8. Q_OBJECT
  9. public:
  10. explicit WorkerThread(QObject *parent = nullptr)
  11. : QThread(parent)
  12. {
  13. qDebug() << "Worker Thread : " << QThread::currentThreadId();
  14. }
  15. ~WorkerThread()
  16. {
  17. // 请求终止
  18. requestInterruption();
  19. quit();
  20. wait();
  21. }
  22. protected:
  23. virtual void run() Q_DECL_OVERRIDE
  24. {
  25. qDebug() << "Worker Run Thread : " << QThread::currentThreadId();
  26. int nValue = 0;
  27. // 是否请求终止
  28. while (!isInterruptionRequested())
  29. {
  30. while (nValue < 100)
  31. {
  32. // 休眠50毫秒
  33. msleep(50);
  34. ++nValue;
  35. // 准备更新
  36. emit resultReady(nValue);
  37. }
  38. }
  39. }
  40. signals:
  41. void resultReady(int value);
  42. };
  43. #endif // WORKERTHREAD_H

在耗时操作中使用 isInterruptionRequested () 来判断是否请求终止线程,如果没有,则一直运行;当希望终止线程的时候,调用 requestInterruption () 即可。这两个函数内部也使用了互斥锁 QMutex。

发表评论

表情:
评论列表 (有 0 条评论,93人围观)

还没有评论,来说两句吧...

相关阅读

    相关 ??0qt 线 进程

    Qt知识回顾(十六)——进程和线程 [Qt知识回顾(十六)——进程和线程\_qt进程\_伴君的博客-CSDN博客][Qt_qt_-CSDN] [Qt_qt_-CSDN

    相关 Qt线基础

    Qt线程基础 Qt是一个功能强大的跨平台应用程序框架,它提供了丰富的多线程支持,使得在Qt应用程序中使用多线程变得简单和高效。在本文中,我们将介绍Qt中的线程基础知识,并提供

    相关 Qt:同步线

    尽管线程的目的是允许代码并行运行,但有时线程必须停止并等待其他线程。比如,如果两个线程 尝试同时写入同一个变量,则结果是不确定的。强制线程相互等待的原理称为互斥。这是保护共享资

    相关 QT线同步

    线程同步 虽然线程的目的是允许代码并行运行,但有时线程必须停止并等待其他线程。例如,如果两个线程试图同时写入同一个变量,结果是未定义的。强制线程彼此等待的原则称为互斥。它是

    相关 Qt线

    今天学习Qt的多线程,在学习多线程主要是两个方面。一是多线程的基础概念,二是多线程的同步,三是怎么和主线程进行通讯。 三.Qt 的应用程序开始执行的时候,只有一个主线程在运行

    相关 QT线

    QT多线程,QT4.7之前是承继 QThread 的方式,比较简单,而 QT5之后,使用了信号和槽的方式,比较灵活。(版本信息可能有误) 下面是对QT线程信息的注意事项