1. 程式人生 > >Qt 之 QThread(深入理解)

Qt 之 QThread(深入理解)

簡述

為了讓程式儘快響應使用者操作,在開發應用程式時經常會使用到執行緒。對於耗時操作如果不使用執行緒,UI介面將會長時間處於停滯狀態,這種情況是使用者非常不願意看到的,我們可以用執行緒來解決這個問題。

前面,已經介紹了QThread常用的兩種方式:

  • Worker-Object
  • 子類化QThread

下面,我們來看看子類化QThread在日常中的應用。

|

大多數情況下,多執行緒耗時操作會與UI進行互動,比如:顯示進度、載入等待。。。讓使用者明確知道目前的狀態,並對結果有一個直觀的預期,甚至有趣巧妙的設計,能讓使用者愛上等待,把等待看成一件很美好的事。

子類化QThread

下面,是一個使用多執行緒操作UI介面的示例 - 更新進度條。與此同時,分享在此過程中有可能遇到的問題及解決方法。

這裡寫圖片描述

定義一個WorkerThread類,讓其繼承自QThread,並重寫run()函式,每隔50毫秒更新當前值,然後發射resultReady()訊號(用於更新進度條)。

#include <QThread>

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); };

構建一個主介面 - 包含按鈕、進度條,當點選“開始”按鈕時,啟動執行緒,更新進度條。

class MainWindow : public CustomWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = 0)
        : CustomWindow(parent)
    {
        qDebug() << "Main Thread : " << QThread::currentThreadId();

        // 建立開始按鈕、進度條
        QPushButton *pStartButton = new QPushButton(this);
        m_pProgressBar = new QProgressBar(this);

        //設定文字、進度條取值範圍
        pStartButton->setText(QString::fromLocal8Bit("開始"));
        m_pProgressBar->setFixedHeight(25);
        m_pProgressBar->setRange(0, 100);
        m_pProgressBar->setValue(0);

        QVBoxLayout *pLayout = new QVBoxLayout();
        pLayout->addWidget(pStartButton, 0, Qt::AlignHCenter);
        pLayout->addWidget(m_pProgressBar);
        pLayout->setSpacing(50);
        pLayout->setContentsMargins(10, 10, 10, 10);
        setLayout(pLayout);

        // 連線訊號槽
        connect(pStartButton, SIGNAL(clicked(bool)), this, SLOT(startThread()));
    }

    ~MainWindow(){}

private slots:
    // 更新進度
    void handleResults(int value)
    {
        qDebug() << "Handle Thread : " << QThread::currentThreadId();
        m_pProgressBar->setValue(value);
    }

    // 開啟執行緒
    void startThread()
    {
        WorkerThread *workerThread = new WorkerThread(this);
        connect(workerThread, SIGNAL(resultReady(int)), this, SLOT(handleResults(int)));
        // 執行緒結束後,自動銷燬
        connect(workerThread, SIGNAL(finished()), workerThread, SLOT(deleteLater()));
        workerThread->start();
    }

private:
    QProgressBar *m_pProgressBar;
    WorkerThread m_workerThread;
};

顯然,UI介面、Worker建構函式、槽函式處於同一執行緒(主執行緒),而run()函式處於另一執行緒(次執行緒)。

Main Thread : 0x34fc
Worker Thread : 0x34fc
Worker Run Thread : 0x4038
Handle Thread : 0x34fc

由於訊號與槽連線型別預設為“Qt::AutoConnection”,在這裡相當於“Qt::QueuedConnection”。

也就是說,槽函式在接收者的執行緒(主執行緒)中執行。

注意:訊號與槽的連線型別,請參考:Qt之Threads和QObjects中“跨執行緒的訊號和槽”部分。

執行緒休眠

上述示例中,通過在run()函式中呼叫msleep(50),執行緒會每隔50毫秒讓當前的進度值加1,然後發射一個resultReady()訊號,其餘時間什麼都不做。在這段空閒時間,執行緒不佔用任何的系統資源。當下一次CPU時鐘來臨時,它會繼續執行。

QThread提供了靜態的、平臺獨立的休眠函式:sleep()、msleep()、usleep(),允許秒,毫秒和微秒來區分,函式接受整型數值作為引數,以表明執行緒掛起執行的時間。當休眠時間結束,執行緒就會獲得CPU時鐘,將繼續執行它的指令。

想象一下,日常用的電腦,如果我們需要離開一段時間,可以將它設定為休眠狀態,為了節約用電,同時響應國家政策 - 走綠色、環保之道。

可以嘗試註釋掉休眠部分的程式碼,這時,由於沒有任何耗時操作,會造成頻繁地更新UI。所以,為了保證介面的流暢性,同時確保進度的更新在人眼可接受的範圍內,我們應在必要的時候加上適當時間的休眠。

在主執行緒中更新UI

當連線方式更改為“Qt::DirectConnection”時:

connect(workerThread, SIGNAL(resultReady(int)), this, SLOT(handleResults(int)), Qt::DirectConnection);

再次點選“開始”按鈕,會很失望,因為它會出現一個異常,描述如下:

ASSERT failure in QCoreApplication::sendEvent: “Cannot send events to objects owned by a different thread. Current thread e346e8. Receiver customWidget’ (of type ‘MainWindow’) was created in thread 4186a0”, file kernel\qcoreapplication.cpp, line 553

顯然,UI介面、Worker建構函式處於同一執行緒(主執行緒),而run()函式、槽函式處於同一執行緒(次執行緒)。

Main Thread : 0x2c6c
Worker Thread : 0x2c6c
Worker Run Thread : 0x4704
Handle Thread : 0x4704

之所以會出現這種情況是因為Qt做了限制(其它大多數GUI程式設計也一樣),不允許在其它執行緒(非主執行緒)中訪問UI控制元件,這麼做主要是怕在多執行緒環境下對介面控制元件進行操作會出現不可預知的情況。

所以,不難理解,由於在槽函式(次執行緒)中更新了UI,所以,會引起以上錯誤。

避免多次connect

當多次點選“開始”按鈕的時候,就會多次connect(),從而啟動多個執行緒,同時更新進度條。

為了避免這個問題,我們修改如下:

class MainWindow : public CustomWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = 0)
        : CustomWindow(parent)
    {
        // ...
        connect(&m_workerThread, SIGNAL(resultReady(int)), this, SLOT(handleResults(int)));
    }

    ~MainWindow(){}

private slots:
    // ...
    void startThread()
    {
        if (!m_workerThread.isRunning())
            m_workerThread.start();
    }

private:
    WorkerThread m_workerThread;
};

將connect新增在建構函式中,保證了訊號槽的正常連線。線上程start()之前,可以使用isFinished()和isRunning()來查詢執行緒的狀態,判斷執行緒是否正在執行,以確保執行緒的正常啟動。

優雅地結束執行緒

如果一個執行緒執行完成,就會結束。可很多情況並非這麼簡單,由於某種特殊原因,當執行緒還未執行完時,我們就想中止它。

不恰當的中止往往會引起一些未知錯誤。比如:當關閉主介面的時候,很有可能次執行緒正在執行,這時,就會出現如下提示:

QThread: Destroyed while thread is still running

這是因為次執行緒還在執行,就結束了UI主執行緒,導致事件迴圈結束。這個問題在使用執行緒的過程中經常遇到,尤其是耗時操作。

在此問題上,常見的兩種人:

  • 直接忽略此問題。
  • 強制中止 - terminate()。

大多數情況下,當程式退出時,次執行緒也許會正常退出。這時,雖然抱著僥倖心理,但隱患依然存在,也許在極少數情況下,就會出現Crash。

正如前面提到過terminate(),比較危險,不鼓勵使用。執行緒可以在程式碼執行的任何點被終止。執行緒可能在更新資料時被終止,從而沒有機會來清理自己,解鎖等等。。。總之,只有在絕對必要時使用此函式。

舉一個簡單的例子:當你的哥們正處於長時間的酣睡狀態時,你想要叫醒他,但是採取的措施卻是潑一盆涼水,想象一下後果?這涼爽 - O(∩_∩)O哈哈~。

所以,我們應該採取合理的措施來優雅地結束執行緒,一般思路:

  1. 發起執行緒退出操作,呼叫quit()或exit()。
  2. 等待執行緒完全停止,刪除建立在堆上的物件。
  3. 適當的使用wait()(用於等待執行緒的退出)和合理的演算法。

下面介紹兩種方式:

  • QMutex互斥鎖 + bool成員變數。

這種方式是Qt4.x中比較常用的,主要是利用“QMutex互斥鎖 + bool成員變數”的方式來保證共享資料的安全性(可以完全參照下面的requestInterruption()原始碼寫法)。

#include <QThread>
#include <QMutexLocker>

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;
};

為什麼要加鎖?很簡單,是為了共享資料段操作的互斥。
何時需要加鎖?在形成資源競爭的時候,也就是說,多個執行緒有可能訪問同一共享資源的時候。

當主執行緒呼叫stop()更新m_bStopped的時候,run()函式也極有可能正在訪問它(這時,他們處於不同的執行緒),所以存在資源競爭,因此需要加鎖,保證共享資料的安全性。

  • Qt5以後:requestInterruption() + isInterruptionRequested()

這兩個介面是Qt5.x引入的,使用很方便:

class WorkerThread : public QThread
{
    Q_OBJECT

public:
    explicit WorkerThread(QObject *parent = 0)
        : QThread(parent)
    {
    }

    ~WorkerThread() {
        // 請求終止
        requestInterruption();
        quit();
        wait();
    }

protected:
    virtual void run() Q_DECL_OVERRIDE {
        // 是否請求終止
        while (!isInterruptionRequested())
        {
            // 耗時操作
        }
    }
};

在耗時操作中使用isInterruptionRequested()來判斷是否請求終止執行緒,如果沒有,則一直執行;當希望終止執行緒的時候,呼叫requestInterruption()即可。

正如侯捷所言:「原始碼面前,了無祕密」。如果還心存疑慮,我們不妨來看看requestInterruption()、isInterruptionRequested()的原始碼:

void QThread::requestInterruption()
{
    Q_D(QThread);
    QMutexLocker locker(&d->mutex);
    if (!d->running || d->finished || d->isInFinish)
        return;
    if (this == QCoreApplicationPrivate::theMainThread) {
        qWarning("QThread::requestInterruption has no effect on the main thread");
        return;
    }
    d->interruptionRequested = true;
}

bool QThread::isInterruptionRequested() const
{
    Q_D(const QThread);
    QMutexLocker locker(&d->mutex);
    if (!d->running || d->finished || d->isInFinish)
        return false;
    return d->interruptionRequested;
}

^_^,內部實現居然也用了互斥鎖QMutex,這樣我們就可以放心地使用了。

更多參考

相關推薦

Qt QThread深入理解

簡述 為了讓程式儘快響應使用者操作,在開發應用程式時經常會使用到執行緒。對於耗時操作如果不使用執行緒,UI介面將會長時間處於停滯狀態,這種情況是使用者非常不願意看到的,我們可以用執行緒來解決這個問題。 前面,已經介紹了QThread常用的兩種方式: Wo

QTQSignalMapper可以理解為轉發器,多個按鈕綁定到一個Edit上,且能分辨。每個單獨連接的話,反而麻煩

this 不同的 lan each b2c etc gpo 知識點 span QT之QSignalMapper QT之QSignalMapper 簡述 效果圖 上代碼 相關知識點文章 結尾 簡述 QSign

Qt 圖形漸變填充

技術 ces 畫筆 win 寬度 pad ora 圖片 angle 簡述 QGradient 可以和 QBrush 組合使用,來指定漸變填充。Qt 目前支持三種類型的漸變填充: QLinearGradient:顯示從起點到終點的漸變 QRadialGradient:以圓心

C++筆記十一——多型的概念和作用深入理解

        多型是面向物件的重要特性,簡單點說:“一個介面,多種實現”,就是同一種事物表現出的多種形態。         程式設計其實就是一個將具體世界進行抽象化的過程,多型就是抽象化的一種體現,把一系列具體事物的共同點抽象出來, 再通過這個抽象的事物, 與不同的具體事物

PyQt5高階介面控制元件QThread十二

QThread 前言 QThread是Qt的執行緒類中最核心的底層類。由於PyQt的的跨平臺特性,QThread要隱藏所有與平臺相關的程式碼 要使用的QThread開始一個執行緒,可以建立它的一個子類,然後覆蓋其它QThread.run()函式

QtQSS暗橙色

https://blog.csdn.net/liang19890820/article/details/54669359#qss-%E6%A0%B7%E5%BC%8F簡述我覺得一個好看的 UI 可以潛意識地激勵我們,不僅可以讓我們工作的更高效、更有樂趣,而且可以讓應用程式變得

CSS深入理解overflowHTML/CSS

clas alt span doc 邊距 蘊含 top overflow 會同 簡介 overflow看上去其貌不揚,其中蘊含的知識點還是很多的,有很多鮮為人知的特性表現。 overflow基本屬性值 1、visible(默認) 2、hidden 3、scroll 4、

springAOP原始碼深入理解aop攔截

(一)  原始碼角度 攔截機 (Interceptor), 是 AOP (Aspect-Oriented Programming) 的另一種叫法。AOP本身是一門語言,只不過我們使用的是基於JAVA的整合到Spring 中的 SpringAOP。同樣,我們將通過我們的例子來理

cocos2dx學習路----第九篇深入理解單點觸控的事件機制

上一篇我們簡單接觸了關於單點觸控的實現。 這一篇我們繼續進一步的理解單點觸控的事件分發的優先順序問題。 我們來回顧一下實現觸控的過程: 1.建立新的監聽器; 2.為新的監聽器分配回撥函式(而我們在上一篇中用到了Lamda表示式來實現); 3.註冊分發監聽器。 好,這一篇就是

深入理解JVM早期編譯期優化

        讀了深入理解JVM之早期優化這一章,JVM作者從編譯期原始碼實現的層次上讓我們瞭解了Java原始碼編譯為位元組碼的過程,分析了Java語言中泛型、主動裝箱/拆箱、條件編譯等多種語法糖的前因後果,並實戰練習瞭如何使用插入式註解處理器來完成一個檢查程式命令規範的

面試舊敵紅黑樹直白介紹深入理解

開發十年,就只剩下這套架構體系了! >>>   

OpenCV探索十六:圖像矯正技術深入探討

double gb2 教科書 長方形 strong fine open lines 導致 剛進入實驗室導師就交給我一個任務,就是讓我設計算法給圖像進行矯正。哎呀,我不太會圖像這塊啊,不過還是接下來了,硬著頭皮開幹吧! 那什麽是圖像的矯正呢?舉個例子就好明白了。 我的好朋友小

2.2 logistic回歸損失函數非常重要,深入理解

問題 好的 為知 得出 cnblogs 回歸算法 很多 將他 深入 上一節當中,為了能夠訓練logistic回歸模型的參數w和b,需要定義一個成本函數 使用logistic回歸訓練的成本函數 為了讓模型通過學習來調整參數,要給出一個含有m和訓練樣本的訓練集很自然的,希望通過

String源碼理解indexOfJDK1.7

static img nta from 來看 png val 四種 targe String的indexOf共有四種參數,分別如下圖: 其中,第一種內部實現如下: public int indexOf(int ch) { return indexOf(c

垃圾收集器與內存分配策略 深入理解JVM二

nali noclass eth 清理 full gc 原因 商業 jit編譯器 代碼 1.概述 垃圾收集(Garbage Collection,GC). 當需要排查各種內存溢出、內存泄露問題時,當垃圾收集成為系統達到更高並發量的瓶頸時,我們就需要對這些&ldquo

QtQEvent所有事件的翻譯

sin ini 隊列 方便 erp 拖放 sdn 所有 area QEvent 類是所有事件類的基類,事件對象包含事件參數。 Qt 的主事件循環(QCoreApplication::exec())從事件隊列中獲取本地窗口系統事件,將它們轉化為 QEvents,然後將轉換後

Java體系介紹深入理解Java虛擬機

方式 java語言 理解 java方法 載器 使用 編譯 三方 但是 網絡帶來的挑戰和機遇: 平臺無關性、安全性和網絡移動性,Java體系的這三方面共同使得Java和發展中的網絡計算環境相得益彰 Java體系結構包括四個獨立但相關的技術: Java程序設計語言 J

Qt學習總結C魚信號與槽01

Qt 學習 總結 C魚 自動關聯 第一種自然是手動關聯了,只要調用connect函數就能實現自動關聯。這裏重點講第二種,自動關聯:為了實現槽函數自動進行關聯,對於Qt窗口部件已經提供的信號,可按照以下規範命名:void on_&lt;窗口部件名稱&gt;_&lt;信號名

Qt學習總結C魚路徑參數引用

Qt學習總結(C魚)之路徑參數引用1.引用相對路徑: 例如: QCursor cursor(QPixmap("1.png")); 問題:會發現引用失敗,這是因為相對路徑都是從當前工作目錄開始找起文件的。可以通過以下函數獲取當前工作目錄: bool QDir::setCurrent ( co

第1章 計算機系統漫遊深入理解計算機系統

printf 保存 並運行 用戶 數據 ogr 語句 亂碼 hello 1 #include <stdio.h> 2 3 int main() 4 { 5 printf("hello, world\n"); 6 } 1.1 信息就是位+上下文 h