Qt之QThread(深入理解)

分類:IT技術 時間:2016-10-17

簡述

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

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

  • Worker-Object
  • 子類化QThread

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

  • 簡述
  • 子類化QThread
  • 線程休眠
  • 在主線程中更新UI
  • 避免多次connect
  • 優雅地結束線程
  • 更多參考

大多數情況下,多線程耗時操作會與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 = http://blog.csdn.net/liang19890820/article/details/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 = http://blog.csdn.net/liang19890820/article/details/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之線程基礎
  • Qt之可重入與線程安全
  • Qt之Threads和QObjects
  • Qt之Concurrent框架
  • Qt之Concurrent Map和Map-Reduce
  • Qt之QThread

Tags: 應用程序 青銅器 國家 blog 九鼎

文章來源:


ads
ads

相關文章
ads

相關文章

ad