zoukankan      html  css  js  c++  java
  • Qt 进程和线程之四:线程实际应用

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

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


    一、多线程操作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()函数处于另一线程(次线程)。


    二、避免多次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()来查询线程的状态,判断线程是否正在运行,以确保线程的正常启动。


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

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

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

    QThread: Destroyed while thread is still running
    

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

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

    1. 发起线程退出操作,调用quit()或exit()。
    2. 等待线程完全停止,删除创建在堆上的对象。
    3. 适当的使用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 之 QThread(深入理解)


  • 相关阅读:
    WHYZOJ-#53 线段树区间修改(线段树)
    洛谷-3373 【模板】线段树 2 (线段树)
    暑假训练-藏妹子之处(递推)
    POJ-1258 Agri-Net(kruskal最小生成树)
    POJ-2559 Largest Rectangle in a Histogram(单调栈)
    BZOJ3439 Kpm的MC密码
    BZOJ3438 小M的作物
    BZOJ3436 小K的农场
    BZOJ3437 小P的牧场
    BZOJ1430 小猴打架
  • 原文地址:https://www.cnblogs.com/linuxAndMcu/p/11078039.html
Copyright © 2011-2022 走看看