一文详解Qt中线程的实际应用

 更新时间:2023年03月10日 10:06:13   作者:音视频开发老舅  
为了让程序尽快响应用户操作,在开发应用程序时经常会使用到线程。这篇文章就来和大家介绍一下Qt中线程的实际应用,感兴趣的小伙伴可以了解一下

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

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

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

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

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

#ifndef WORKERTHREAD_H
#define WORKERTHREAD_H
 
#include <QThread>
#include <QDebug>
 
class WorkerThread : public QThread
{
    Q_OBJECT
 
public:
    explicit WorkerThread(QObject *parent = 0)
        : QThread(parent)
    {
        qDebug() << "Worker Thread : " << QThread::currentThreadId();
    }
 
protected:
    virtual void run() Q_DECL_OVERRIDE
    {
        qDebug() << "Worker Run Thread : " << QThread::currentThreadId();
        int nValue = 0;
        while (nValue < 100)
        {
            // 休眠50毫秒
            msleep(50);
            ++nValue;
 
            // 准备更新
            emit resultReady(nValue);
        }
    }
 
signals:
    void resultReady(int value);
};
 
#endif // WORKERTHREAD_H

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

再在mainwindow.ui上添加一个按钮和进度条部件,然后mainwindow.h修改如下:

#ifndef MAINWINDOW_H
#define MAINWINDOW_H
 
#include <QMainWindow>
#include "workerthread.h"
 
namespace Ui {
class MainWindow;
}
 
class MainWindow : public QMainWindow
{
    Q_OBJECT
 
public:
    explicit MainWindow(QWidget *parent = nullptr);
    ~MainWindow();
 
private slots:
    // 更新进度
    void handleResults(int value);
 
    // 开启线程
    void startThread();
 
private:
    Ui::MainWindow *ui;
 
    WorkerThread m_workerThread;
};
 
#endif // MAINWINDOW_H

然后mainwindow.cpp修改如下:

#include "mainwindow.h"
#include "ui_mainwindow.h"
 
MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
        
    qDebug() << "Main Thread : " << QThread::currentThreadId();        
 
    // 连接信号槽
    this->connect(ui->pushButton, SIGNAL(clicked(bool)), this, SLOT(startThread()));
}
 
MainWindow::~MainWindow()
{
    delete ui;
}
 
void MainWindow::handleResults(int value)
{
    qDebug() << "Handle Thread : " << QThread::currentThreadId();
    ui->progressBar->setValue(value);
}
 
void MainWindow::startThread()
{
    WorkerThread *workerThread = new WorkerThread(this);
    this->connect(workerThread, SIGNAL(resultReady(int)), this, SLOT(handleResults(int)));
    // 线程结束后,自动销毁
    this->connect(workerThread, SIGNAL(finished()), workerThread, SLOT(deleteLater()));
    workerThread->start();
}

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

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

Main Thread :  0x3140
Worker Thread :  0x3140
Worker Run Thread :  0x2588
Handle Thread :  0x3140

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

2、避免多次connect

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

#include "mainwindow.h"
#include "ui_mainwindow.h"
 
MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
 
    // 连接信号槽
    this->connect(ui->pushButton, SIGNAL(clicked(bool)), this, SLOT(startThread()));
 
    this->connect(&m_workerThread, SIGNAL(resultReady(int)), this, SLOT(handleResults(int)));
}
 
MainWindow::~MainWindow()
{
    delete ui;
}
 
void MainWindow::handleResults(int value)
{
    qDebug() << "Handle Thread : " << QThread::currentThreadId();
    ui->progressBar->setValue(value);
}
 
void MainWindow::startThread()
{
    if (!m_workerThread.isRunning())
        m_workerThread.start();
}

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

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

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

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

QThread: Destroyed while thread is still running

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

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

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

方法一

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

#ifndef WORKERTHREAD_H
#define WORKERTHREAD_H
 
#include <QThread>
#include <QMutexLocker>
#include <QDebug>
 
class WorkerThread : public QThread
{
    Q_OBJECT
 
public:
    explicit WorkerThread(QObject *parent = 0)
        : QThread(parent),
          m_bStopped(false)
    {
        qDebug() << "Worker Thread : " << QThread::currentThreadId();
    }
 
    ~WorkerThread()
    {
        stop();
        quit();
        wait();
    }
 
    void stop()
    {
        qDebug() << "Worker Stop Thread : " << QThread::currentThreadId();
        QMutexLocker locker(&m_mutex);
        m_bStopped = true;
    }
 
protected:
    virtual void run() Q_DECL_OVERRIDE 
    {
        qDebug() << "Worker Run Thread : " << QThread::currentThreadId();
        int nValue = 0;
        while (nValue < 100)
        {
            // 休眠50毫秒
            msleep(50);
            ++nValue;
 
            // 准备更新
            emit resultReady(nValue);
 
            // 检测是否停止
            {
                QMutexLocker locker(&m_mutex);
                if (m_bStopped)
                    break;
            }
            // locker超出范围并释放互斥锁
        }
    }
    
signals:
    void resultReady(int value);
 
private:
    bool m_bStopped;
    QMutex m_mutex;
};
 
#endif // WORKERTHREAD_H

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

为什么要加锁?

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

方法二

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

#ifndef WORKERTHREAD_H
#define WORKERTHREAD_H
 
#include <QThread>
#include <QMutexLocker>
#include <QDebug>
 
class WorkerThread : public QThread
{
    Q_OBJECT
 
public:
    explicit WorkerThread(QObject *parent = nullptr)
        : QThread(parent)
    {
        qDebug() << "Worker Thread : " << QThread::currentThreadId();
    }
 
    ~WorkerThread()
    {
        // 请求终止
        requestInterruption();
        quit();
        wait();
    }
 
protected:
    virtual void run() Q_DECL_OVERRIDE
    {
        qDebug() << "Worker Run Thread : " << QThread::currentThreadId();
        int nValue = 0;
 
        // 是否请求终止
        while (!isInterruptionRequested())
        {
            while (nValue < 100)
            {
                // 休眠50毫秒
                msleep(50);
                ++nValue;
 
                // 准备更新
                emit resultReady(nValue);
            }
        }
 
    }
signals:
    void resultReady(int value);
};
 
#endif // WORKERTHREAD_H

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

以上就是一文详解Qt中线程的实际应用的详细内容,更多关于Qt线程的资料请关注脚本之家其它相关文章!

相关文章

  • C语言函数的基本使用和递归详解

    C语言函数的基本使用和递归详解

    一个函数在它的函数体内调用它自身称为递归调用。这种函数称为递归函数。C语言允许函数的递归调用。在递归调用中,主调函数又是被调函数。执行递归函数将反复调用其自身,每调用一次就进入新的一层
    2021-09-09
  • C/C++实现日期计算器的示例代码

    C/C++实现日期计算器的示例代码

    本篇文章主要介绍了C/C++实现日期计算器的示例代码,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-09-09
  • C/C++ 引用作为函数的返回值方式

    C/C++ 引用作为函数的返回值方式

    这篇文章主要介绍了C/C++ 引用作为函数的返回值方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-07-07
  • 带你了解C++的IO流

    带你了解C++的IO流

    这篇文章主要介绍了C++ IO流的相关资料,帮助大家更好的理解和学习c++,感兴趣的朋友可以了解下,希望能够给你带来帮助
    2021-09-09
  • 基于Windows API实现遍历所有文件并删除的方法

    基于Windows API实现遍历所有文件并删除的方法

    这篇文章主要介绍了基于Windows API实现遍历所有文件并删除的方法,是win32应用程序的一个比较典型的文件操作应用技巧,需要的朋友可以参考下
    2015-04-04
  • C++常量指针,指针常量,指向常量的常指针详解

    C++常量指针,指针常量,指向常量的常指针详解

    刚接触到指针时,关于C++常量指针,指针常量,指向常量的常指针容易混淆,所以整理下,希望能够帮助自己也帮助到大家
    2021-10-10
  • vc++实现的tcp socket客户端和服务端示例

    vc++实现的tcp socket客户端和服务端示例

    这篇文章主要介绍了vc++实现的tcp socket客户端和服务端示例,需要的朋友可以参考下
    2014-03-03
  • C++读取文本文件中的汉字乱码情况原因及解决

    C++读取文本文件中的汉字乱码情况原因及解决

    本文介绍简体中文Windows操作系统中,C++读取文本文件中的汉字乱码情况原因及解决,文中通过代码和图文给大家介绍的非常详细,具有一定的参考价值,需要的朋友可以参考下
    2024-01-01
  • c语言简单实现文件 r/w 操作方法

    c语言简单实现文件 r/w 操作方法

    由于在 C 语言中 '\' 一般是转义字符的起始标志,故在路径中需要用两个 '\' 表示路径中目录层次的间隔,也可以使用 '/' 作为路径中的分隔符,本文重点给大家介绍用c语言简单实现文件 r/w 操作方法,感兴趣的朋友一起学习吧
    2021-05-05
  • C++示例分析内联函数与引用变量及函数重载的使用

    C++示例分析内联函数与引用变量及函数重载的使用

    为了消除函数调用的时空开销,C++ 提供一种提高效率的方法,即在编译时将函数调用处用函数体替换,类似于C语言中的宏展开。这种在函数调用处直接嵌入函数体的函数称为内联函数(Inline Function),又称内嵌函数或者内置函数
    2022-08-08

最新评论