使用Qt中的QThread創建線程


上篇文章中簡單介紹瞭如何使用 Windows API 和c++11中的 std::thread 創建線程。
線程的創建和基本使用
本篇文章將會介紹如何使用QThread創建線程。

  • QThread是Qt所有線程控制的基礎,每一個QThread實例對象控制一個線程。
  • QThread可以直接實例化使用也可以用繼承的方式使用,QThread以事件循環的方式,允許繼承自QObject的槽函數在線程中被調用執行。子類化QThread可以在開啓線程事件循環之前初始化一個新線程,或者不使用事件循環的方式執行並行代碼。

1. 使用信號和槽的形式觸發

QThread的入口執行函數是 run() 函數,默認 run() 函數會通過調用函數 exec() 開啓事件循環在線程中。可以使用函數 QObject::moveToThread() 將一個工作對象與線程對象相關聯。
下面是一個簡單的示例,示例中在一個新線程中計算前n個數的和後通過信號返回給調用者:
工作類頭文件, Worker.h

#ifndef WORKER_H
#define WORKER_H

#include <QObject>
class Worker : public QObject
{
    Q_OBJECT

public:
    Worker(QObject* parent = nullptr);
    ~Worker();

public slots:
    // 計算前count個數的和
    void doWork(int count);

signals:
    // 發送計算完成信號
    void doFinished(int);
};

#endif

工作類CPP文件, Worker.cpp

#include "Worker.h"
#include <QDebug>
#include <QThread>

Worker::Worker(QObject* parent)
    :QObject(parent)
{

}

Worker::~Worker()
{

}

// 計算 0~count個數的和
void Worker::doWork(int count)
{
    int sum = 0;
    for (int i=0; i<=count; ++i)
        sum += i;

	// 打印當前函數名,線程ID,以及計算結果
    qDebug() << __FUNCTION__ << "Thread ID: " << QThread::currentThreadId() << ", Result is " << sum;

    emit doFinished(sum);
}

槽函數 void doWork(int count); 用來計算前count個數的和,計算完成後,發送信號 doFinished(int) 其中的參數是計算結果。這就是一個工作類,與線程一點關係沒有。

接下來是控制器
控制器頭文件,Controller.h

#ifndef MYOBJECT_H
#define MYOBJECT_H

#include <QObject>
#include <QThread>

class Controller : public QObject
{
    Q_OBJECT

public:
    Controller(QObject* parent = nullptr);
    ~Controller();

    // 開啓線程計算
    void startThreadRunFunc(int number);

private:
    QThread m_thread;

signals:
	// 該信號用於觸發工作者中的槽函數
    void startCalcSum(int);

private slots:
    // 接受計算完畢後的結果槽函數
    void onCalcSumFinished(int sum);
};

#endif

控制器CPP文件,Controller.cpp

#include "Controller.h"
#include "Worker.h"
#include <QDebug>

Controller::Controller(QObject* parent)
    :QObject (parent)
{
	// [1]
    Worker* worker = new Worker;
    worker->moveToThread(&m_thread);

	// [2]
    QObject::connect(this, &Controller::startCalcSum, worker, &Worker::doWork);
    // [3]
    QObject::connect(worker, &Worker::doFinished, this, &Controller::onCalcSumFinished);

    // [4] 當線程退出時,釋放工作者內存
    QObject::connect(&m_thread, &QThread::finished, worker, &Worker::deleteLater);

	// [5]
    m_thread.start();
}

Controller::~Controller()
{
    m_thread.quit();
    m_thread.wait();
}

void Controller::startThreadRunFunc(int number)
{
    // 發送開始計算信號
    emit startCalcSum(number);
    qDebug() << __FUNCTION__ << " : Current Thread is " << QThread::currentThreadId();
}

void Controller::onCalcSumFinished(int sum)
{
    // 打印行數名,當前線程ID,計算結果
    qDebug() << __FUNCTION__ \
             << " : Current Thread is " << QThread::currentThreadId() \
             << ", Result is " << sum;
}

構造函數中,主要做了如下步驟:

  1. 首先創建工作者對象,並與線程相關聯。
  2. 連接控制器的 startCalcSum 信號和工作者的 doWork 槽函數,即發送 startCalcSum 信號時,觸發 doWork 槽函數。這裏要說明的是,因爲是跨線程的信號和槽的鏈接,這裏默認的鏈接方式是使用 隊列連接 。具體信號和槽的鏈接方式可參考 Qt中的信號和槽
  3. 連接工作者的 doFinished 信號和控制器的 onCalcSumFinished 函數。當計算完成時,會觸發 doFinished 信號,同樣這裏也是 隊列連接 的方式。
  4. 連接線程的 finished 信號和工作者的 deleteLater 槽函數。當線程中不再有事件被執行並且事件循環停止退出的時候,QThread發送該 finished 信號。
  5. 調用函數 start() 開啓線程。

main函數中代碼如下:

#include <QCoreApplication>
#include <QThread>
#include "Controller.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

	// 創建控制器
    Controller *object = new Controller;
    // 計算前100個數的和
    object->startThreadRunFunc(100);

    return a.exec();
}

執行結果如下:
Controller::startThreadRunFunc : Current Thread is 0x491c
Worker::doWork Thread ID: 0x62c0 , Result is 5050
Controller::onCalcSumFinished : Current Thread is 0x491c , Result is 5050

整體流程如下:

  1. 函數 startThreadRunFunc 發送信號 startCalcSum ,此過程在主線程中執行。
  2. 觸發 doWork 槽函數,計算前100個數的和,併發送信號 doFinished 信號,此過程在新建的線程中執行。
  3. 觸發 onCalcSumFinished 槽函數,此過程在主線程中執行。

2. 使用繼承自QThread方式觸發

在Qt4.x的時候,QThread的常用方式是繼承QThread重載函數 run()run() 函數是新線程的入口函數。我們同樣完成上功能,代碼如下:
CThread頭文件:

#ifndef CTHREAD_H
#define CTHREAD_H

#include <QThread>
#include <atomic>
class CThread : public QThread
{
    Q_OBJECT

public:
    CThread(QObject* parent = nullptr);
    ~CThread();

    // 線程入口函數
    void run(void) override;

    // 計算前 0 ~ number的和
    void calcSum(int number);

private:
    std::atomic<bool> m_startThread;
    std::atomic<int> m_number;

signals:
    // 發送計算完成信號
    void doFinished(int);

private slots:
    // 相應計算完成結果
    void onDoFinished(int sum);
};

#endif

CThread源文件

#include "CThread.h"
#include <QDebug>

CThread::CThread(QObject* parent)
    :QThread (parent)
    ,m_startThread(false)
    ,m_number(0)
{
    QObject::connect(this, &CThread::doFinished, this, &CThread::onDoFinished);
    this->start();
}

CThread::~CThread()
{
    this->requestInterruption();
    this->wait();
}

void CThread::run(void)
{
    while (!this->isInterruptionRequested())
    {
        // 判斷是否開啓線程計算
        if (!m_startThread)
        {
            QThread::msleep(20);
            continue;
        }

        // 計算 0 ~ m_number的和
        int number = m_number;
        int sum = 0;
        for (int i = 0; i<=number; ++i)
            sum += i;

        // 打印函數名,線程ID,結果
        qDebug() << __FUNCTION__ \
                 << " : Current Thread Id is " << QThread::currentThreadId() \
                 << ", Result is " << sum;

        m_startThread = false;

        // 發送信號
        emit doFinished(sum);
    }
}

// 計算前 0 ~ number的和
void CThread::calcSum(int number)
{
    m_number = number;
    m_startThread = true;
}

void CThread::onDoFinished(int sum)
{
    // 打印函數名,線程ID,結果
    qDebug() << __FUNCTION__ \
             << " : Current Thread Id is " << QThread::currentThreadId() \
             << ", Result is " << sum;
}

run() 函數,循環執行

  1. 函數 isInterruptionRequested() 默認值爲false,放調用函數 requestInterruption() 函數時,isInterruptionRequested() 的返回值爲true,且這兩個函數都是線程安全的。
  2. 通過變量 m_startThread 判斷是否需要執行計算,這是一個 std::atomic<bool> 類型的變量,爲原子量,爲了保證共享內容的線程安全。
  3. 計算併發送信號 doFinished()

調用部分如下:

#include <QCoreApplication>
#include "CThread.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

	// 使用繼承QThread的方式開啓線程計算前100個數的和
    CThread *thread = new CThread;
    thread->calcSum(100);

    return a.exec();
}

結果如下:
CThread::run : Current Thread Id is 0x68c0 , Result is 5050
CThread::onDoFinished : Current Thread Id is 0x5ce0 , Result is 5050

分析:

  1. 調用函數 calcSum 函數,在新線程中計算前100個數的和併發送信號 doFinished
  2. 主線程接收信號,並執行槽函數 onDoFinished

3. 幾點說明

關於QThread我個人認知的一點點說明:

(1)使用信號和槽的方式是Qt的推薦方式,有兩點好處:

  • 可以分離線程和具體實現,比如工作者對象可以在單獨的線程中執行,也可以在主線程中執行。
  • 可以有多個線程的函數入口,創建多少個槽函數就有多少個線程函數入口。

(2)關於線程的等待退出

  • 信號和槽的方式,使用如下代碼:
m_thread.quit();
m_thread.wait();

quit() 函數會退出事件循環,wait() 函數阻塞等待線程退出。

  • 繼承QThread的方式,使用如下代碼:
this->requestInterruption();
this->wait();

當使用 isInterruptionRequested()run() 函數作爲循環條件時,可以先請求退出,然後再阻塞等待線程的退出。

(3) 線程對象和線程是兩個不同的概念。比如上面的例子

CThread *thread = new CThread;

thread 對象就是一個線程對象,該對象的歸屬是主線程。因此該線程對象的槽函數的執行是在主線程中的;使用函數 moveToThread() 是更改對象的歸屬線程,因此信號和槽的方式觸發函數的執行是在新線程中。值得注意的是,線程中實現槽函數的觸發,必須需要執行事件循環即 exec() 函數。

(4)GUI的相關操作只能在主線程中完成。
QWidget等對象的創建和操作必須在主線程中完成,其他的非界面相關的類可以在不同的線程中操作。 moveToThread() 的對象及其父對象必須在同一個線程中。


作者:douzhq
個人主頁:http://www.douzhq.cn
文章同步頁(文章末尾可下載代碼): http://www.douzhq.cn/thread_qthread/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章