1. 程式人生 > >【Qt】Qt的執行緒(兩種QThread類的詳細使用方式)

【Qt】Qt的執行緒(兩種QThread類的詳細使用方式)

Qt提供QThread類以進行多工處理。與多工處理一樣,Qt提供的執行緒可以做到單個執行緒做不到的事情。例如,網路應用程式中,可以使用執行緒處理多種聯結器。

QThread繼承自QObject類,且提供QMutex類以實現同步。執行緒和程序共享全域性變數,可以使用互斥體對改變後的全域性變數值實現同步。因此,必須編輯全域性資料時,使用互斥體實現同步,其它程序則不能改變或瀏覽全域性變數值。

什麼是互斥體?

互斥體實現了“互相排斥”(mutual exclusion)同步的簡單形式(所以名為互斥體(mutex))。互斥體禁止多個執行緒同時進入受保護的程式碼“臨界區”(critical section)。

在任意時刻,只有一個執行緒被允許進入程式碼保護區。任何執行緒在進入臨界區之前,必須獲取(acquire)與此區域相關聯的互斥體的所有權。如果已有另一執行緒擁有了臨界區的互斥體,其他執行緒就不能再進入其中。這些執行緒必須等待,直到當前的屬主執行緒釋放(release)該互斥體。

什麼時候需要使用互斥體呢?

互斥體用於保護共享的易變程式碼,也就是,全域性或靜態資料。這樣的資料必須通過互斥體進行保護,以防止它們在多個執行緒同時訪問時損壞。

 

Qt執行緒基礎

QThread的建立和啟動

class MyThread : public QThread
{
    Q_OBJECT
protected:
    void run();
};

void MyThread :: run(){
    ...
}

如上述程式碼所示,如果要建立執行緒,則必須繼承QThread類。MyThread使用成員函式run()才會實現執行緒。

Qt提供的執行緒類

Qt提供的執行緒類
執行緒類 說明
QAtomicInt 提供了Integer上與平臺無關的Qtomic運算
QAtomicPointer 提供了指標上Atomic運算的模板函式
QFuture 顯示非同步運算結果的類
QFutureSynchronizer QFuture類簡化同步而提供的類
QFutureWatcher 使用訊號和槽,允許QFuture監聽
QMutex 訪問類之間的同步
QMutecLocker 簡化Lock和Unlock Mutex的類
QReadWriteLock 控制讀寫操作的類
QReadLocker 為了讀訪問而提供的
QWriteLocker 為了寫訪問而提供的
QRunnable 正在執行的所有物件的父類,且定義了虛擬函式run()
QSemaphore 一般的Count互斥體類
QThread 提供與平臺無關的執行緒功能的類
QThreadPool 管理執行緒的類
QThreadStorage 提供每個執行緒儲存區域的類
QWaitCondition 確認執行緒間同步的類的狀態值

同步QThread的類

為了同步執行緒,Qt提供了QMutex、QReadWriteLock、QSemaphore和QWaitCondition類。主執行緒等待與其他執行緒的中斷時,必須進行同步。例如:兩個執行緒同時訪問共享變數,那麼可能得不到預想的結果。因此,兩個執行緒訪問共享變數時,必須進行同步。

  1. 一個執行緒訪問指定的共享變數時,為了禁止其他執行緒訪問,QMutex提供了類似鎖定裝置的功能。互斥體啟用狀態下,執行緒不能同時訪問共享變數,必須在先訪問的執行緒完成訪問後,其他執行緒才可以繼續訪問。
  2. 一個執行緒訪問互斥體鎖定的共享變數期間,如果其他執行緒也訪問此共享變數,那麼該執行緒將會一直處於休眠狀態,直到正在訪問的執行緒結束訪問。這稱為執行緒安全。
  3. QReadWriteLock和QMutex的功能相同,區別在於,QReadWriteLock對資料的訪問分為讀訪問和寫訪問。很多執行緒頻繁訪問共享變數時,與QMetex相對,使用QReadWriteLock更合適。
  4. QSemaphore擁有和QMutex一樣的同步功能,可以管理多個按數字識別的資源。QMutex只能管理一個資源,但如果使用QSemaphore,則可以管理多個按號碼識別的資源。
  5. 條件符合時,QWaitCondition允許喚醒執行緒。例如,多個執行緒中某個執行緒被阻塞時,通過QWaitCondition提供的函式wakeOne()和wakeAll()可以喚醒該執行緒。

可重入性與執行緒安全

  • 可重入性:兩個以上執行緒並行訪問時,即使不按照呼叫順序重疊執行程式碼,也必須保證結果;
  • 執行緒安全:執行緒並行執行的情況下,雖然保證可以使程式正常執行,但訪問靜態空間或共享(堆等記憶體物件)物件時,要使用互斥體等機制保證結果。

一個執行緒安全的函式不一定是可重入的;一個可重入的函式缺也不一定是執行緒安全的!

可重入函式主要用於多工環境中,一個可重入的函式簡單來說就是可以被中斷的函式,也就是說,可以在這個函式執行的任何時刻中斷它,轉入OS排程下去執行另外一段程式碼,而返回控制時不會出現什麼錯誤;而不可重入的函式由於使用了一些系統資源,比如全域性變數區,中斷向量表等,所以它如果被中斷的話,可能會出現問題,這類函式是不能執行在多工環境下的。

編寫可重入函式時,若使用全域性變數,則應通過關中斷、訊號量(即P、V操作)等手段對其加以保護。若對所使用的全域性變數不加以保護,則此函式就不具有可重入性,即當多個執行緒呼叫此函式時,很有可能使有關全域性變數變為不可知狀態。

滿足下列條件的函式多數是不可重入的:

  • 函式體內使用了靜態的資料結構和全域性變數,若必須訪問全域性變數,利用互斥訊號量來保護全域性變數;;
  • 函式體內呼叫了malloc()或者free()函式;
  • 函式體內呼叫了標準I/O函式。

常見的不可重入函式有:

  • printf --------引用全域性變數stdout
  • malloc --------全域性記憶體分配表
  • free    --------全域性記憶體分配表

也就是說:本質上,可重入性與C++類或者沒有全域性靜態變數的函式相似,由於只能訪問自身所有的資料變數區域,所以即使有兩個以上執行緒訪問,也可以保證安全性。

QThread和QObjects

QThread類繼承自QObjects類。因此,執行緒開始或結束時,QThread類發生傳送訊號事件。訊號與槽的功能是QThread類從QObject類繼承的,可以通過訊號與槽處理開始或結束等操作,所以可以實現多執行緒。QObject是基於QTimer、QTcpSocket、QUdpSocket和QProcess之類的非圖形使用者介面的子類。

基於非圖形使用者介面的子類可以無執行緒操作。單一類執行某功能時,可以不需要執行緒。但是,執行單一類的目標程式的上級功能時,則必須通過執行緒實現。

執行緒A和執行緒B沒有結束的情況下,應設計使主執行緒時間迴圈不結束;而若執行緒A遲遲不結束而導致主執行緒迴圈也遲遲不能結束,故也要防止執行緒A沒有在一定時間內結束。

處理QThread的訊號和槽的型別

Qt提供了可以決定訊號與槽型別的列舉類,以線上程環境中適當處理事物。

決定訊號與槽型別的列舉類
常量 說明
Qt::AutoConnection 0 如果其他執行緒中發生訊號,則會插入佇列,像QueuedConnection一樣,否則如DirectConnection一樣,直接連線到槽。傳送訊號時決定Connection型別。
Qt::DirectConnection 1 發生訊號事件後,槽立即響應
Qt::QueuedConnection 2 返回收到的執行緒事件迴圈時,發生槽事件。槽在收到的執行緒中執行
Qt::BlockingQueuedConnection 3 與QueuedConnection一樣,返回槽時,執行緒被阻塞。建立在事件發生處使用該型別

使用QtConcurrent類的並行程式設計

QtConcurrent類提供多執行緒功能,不使用互斥體、讀寫鎖、等待條件和訊號量等低階執行緒。使用QtConcurrent建立的程式會根據程序數自行調整使用的執行緒數。

 

QThread類

簡述

QThread類提供了與系統無關的執行緒。

QThread代表在程式中一個單獨的執行緒控制。執行緒在run()中開始執行,預設情況下,run()通過呼叫exec()啟動事件迴圈並在執行緒裡執行一個Qt的事件迴圈。

詳細描述

QThread類可以不受平臺影響而實現執行緒。QThread提供在程式中可以控制和管理執行緒的多種成員函式和訊號/槽。通過QThread類的成員函式start()啟動執行緒。

QThread通過訊號函式started()和finished()通知開始和結束,並檢視執行緒狀態;可以使用isFinished()和isRunning()來查詢執行緒的狀態;使用函式exit()和quit()可以結束執行緒。

如果使用多執行緒,有時需要等到所有執行緒終止。此時,使用函式wait()即可。執行緒中,使用成員函式sleep()、msleep()和usleep()可以暫停秒、毫秒及微秒單位的執行緒。

一般情況下,wait()和sleep()函式應該不需要,因為Qt是一個事件驅動型框架。考慮監聽finished()訊號來取代wait(),使用QTimer來取代sleep()。

靜態函式currentThreadId()和currentThread()返回標識當前正在執行的執行緒。前者返回該執行緒平臺特定的ID,後者返回一個執行緒指標。

要設定執行緒的名稱,可以在啟動執行緒之前呼叫setObjectName()。如果不呼叫setObjectName(),執行緒的名稱將是執行緒物件的執行時型別(QThread子類的類名)。

執行緒管理

可以將常用的介面按照功能進行以下分類:

執行緒啟動

void start(Priority priority = InheritPriority) [slot] 

呼叫後會執行run()函式,但在run()函式執行前會發射訊號started(),作業系統將根據優先順序引數排程執行緒。如果執行緒已經在執行,那麼這個函式什麼也不做。優先順序引數的效果取決於作業系統的排程策略。特別是那些不支援執行緒優先順序的系統優先順序將會被忽略(例如在Linux中,更多細節請參考http://linux.die.net/man/2/sched_setscheduler)。

執行緒執行

int exec() [protected] 

進入事件迴圈並等待直到呼叫exit(),返回值是通過呼叫exit()來獲得,如果呼叫成功則範圍0。

void run() [virtual protected] 

執行緒的起點,在呼叫start()之後,新建立的執行緒就會呼叫這個函式,預設實現呼叫exec(),大多數需要重新實現這個函式,便於管理自己的執行緒。該方法返回時,該執行緒的執行將結束。

執行緒退出

void quit() [slot] 

告訴執行緒事件迴圈退出,返回0表示成功,相當於呼叫了QThread::exit(0)。

void exit(int returnCode = 0) 

告訴執行緒事件迴圈退出。 呼叫這個函式後,執行緒離開事件迴圈後返回,QEventLoop::exec()返回returnCode,按照慣例,0表示成功;任何非0值表示失敗。

void terminate() [slot] 

終止執行緒,執行緒可能會立即被終止也可能不會,這取決於作業系統的排程策略,使用terminate()之後再使用QThread::wait(),以確保萬無一失。當執行緒被終止後,所有等待中的執行緒將會被喚醒。 

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

void requestInterruption() 

請求執行緒的中斷。該請求是諮詢意見並且取決於執行緒上執行的程式碼,來決定是否及如何執行這樣的請求。此函式不停止執行緒上執行的任何事件迴圈,並且在任何情況下都不會終止它。

執行緒等待

void msleep(unsigned long msecs) [static]     //強制當前執行緒睡眠msecs毫秒

void sleep(unsigned long secs) [static]     //強制當前執行緒睡眠secs秒

void usleep(unsigned long usecs) [static]     //強制當前執行緒睡眠usecs微秒

bool wait(unsigned long time = ULONG_MAX)     //執行緒將會被阻塞,等待time毫秒。和sleep不同的是,如果執行緒退出,wait會返回。

執行緒狀態

bool isFinished() const     //執行緒是否結束

bool isRunning() const     //執行緒是否正在執行

bool isInterruptionRequested() const     //如果執行緒上的任務執行應該停止,返回true。可以使用requestInterruption()請求中斷。 

//此函式可用於使長時間執行的任務乾淨地中斷。從不檢查或作用於該函式返回值是安全的,但是建議在長時間執行的函式中經常這樣做。注意:不要過於頻繁呼叫,以保持較低的開銷。

執行緒優先順序

void setPriority(Priority priority) 

設定正在執行執行緒的優先順序。如果執行緒沒有執行,此函式不執行任何操作並立即返回。使用的start()來啟動一個執行緒具有特定的優先順序。優先順序引數可以是QThread::Priority列舉除InheritPriortyd的任何值。

Qt多執行緒優先順序
常量 優先順序
QThread::IdlePriority 0 沒有其它執行緒執行時才排程
QThread::LowestPriority 1 比LowPriority排程頻率低
QThread::LowPriority 2 比NormalPriority排程頻率低
QThread::NormalPriority 3 作業系統的預設優先順序
QThread::HighPriority 4 比NormalPriority排程頻繁
QThread::HighestPriority 5 比HighPriority排程頻繁
QThread::TimeCriticalPriority 6 儘可能頻繁的排程
QThread::InheritPriority 7 使用和建立執行緒同樣的優先順序. 這是預設值

 

QThread類使用方式

QThread的使用方法有如下兩種:

  • QObject::moveToThread()
  • 繼承QThread類

QObject::moveToThread

方法描述:

  1. 定義一個繼承於QObject的worker類,在worker類中定義一個槽slot函式doWork(),這個函式中定義執行緒需要做的工作;
  2. 在要使用執行緒的controller類中,新建一個QThread的物件和woker類物件,使用moveToThread()方法將worker物件的事件迴圈全部交由QThread物件處理;
  3. 建立相關的訊號函式和槽函式進行連線,然後發出訊號觸發QThread的槽函式,使其執行工作。

例子:

#ifndef WORKER_H
#define WORKER_H
#include <QObject>
#include<QDebug>
#include<QThread>
class Worker:public QObject                    //work定義了執行緒要執行的工作
{
    Q_OBJECT
public:
    Worker(QObject* parent = nullptr){}
public slots:
    void doWork(int parameter)                        //doWork定義了執行緒要執行的操作
    {
        qDebug()<<"receive the execute signal---------------------------------";
        qDebug()<<"     current thread ID:"<<QThread::currentThreadId();
       for(int i = 0;i!=1000000;++i)
       {
        ++parameter;
       }
       qDebug()<<"      finish the work and sent the resultReady signal\n";
       emit resultReady(parameter);           //emit啥事也不幹,是給程式設計師看的,表示發出訊號發出訊號
    }

signals:
    void resultReady(const int result);               //執行緒完成工作時傳送的訊號
};

#endif // WORKER_H
#ifndef CONTROLLER_H
#define CONTROLLER_H
#include <QObject>
#include<QThread>
#include<QDebug>
class Controller : public QObject            //controller用於啟動執行緒和處理執行緒執行結果
{
    Q_OBJECT
    QThread workerThread;
public:
    Controller(QObject *parent= nullptr);
    ~Controller();
public slots:
    void handleResults(const int rslt)                        //處理執行緒執行的結果
    {
        qDebug()<<"receive the resultReady signal---------------------------------";
        qDebug()<<"     current thread ID:"<<QThread::currentThreadId()<<'\n';
        qDebug()<<"     the last result is:"<<rslt;
    }
signals:
    void operate(const int);                        //傳送訊號觸發執行緒
};

#endif // CONTROLLER_H
#include "controller.h"
#include <worker.h>
Controller::Controller(QObject *parent) : QObject(parent)
{
    Worker *worker = new Worker;
    worker->moveToThread(&workerThread);            //呼叫moveToThread將該任務交給workThread

    connect(this, SIGNAL(operate(const int)), worker, SLOT(doWork(int)));            //operate訊號發射後啟動執行緒工作
    connect(&workerThread, &QThread::finished, worker, &QObject::deleteLater);            //該執行緒結束時銷燬
    connect(worker, SIGNAL(resultReady(int)), this, SLOT(handleResults(int)));            //執行緒結束後傳送訊號,對結果進行處理

    workerThread.start();                //啟動執行緒
    qDebug()<<"emit the signal to execute!---------------------------------";
    qDebug()<<"     current thread ID:"<<QThread::currentThreadId()<<'\n';
    emit operate(0);
}

Controller::~Controller()        //解構函式中呼叫quit()函式結束執行緒
{
    workerThread.quit();
    workerThread.wait();
}

繼承QThread類

方法描述

  • 自定義一個繼承QThread的類MyThread,過載MyThread中的run()函式,在run()函式中寫入需要執行的工作;
  • 呼叫start()函式來啟動執行緒。

例子:

#ifndef MYTHREAD_H
#define MYTHREAD_H
#include<QThread>
#include<QDebug>
class MyThread : public QThread
{
    Q_OBJECT
public:
    MyThread(QObject* parent = nullptr);
signals:                //自定義傳送的訊號
    void myThreadSignal(const int);
public slots:                //自定義槽
    void myThreadSlot(const int);
protected:
    void run() override;
};

#endif // MYTHREAD_H
#include "mythread.h"

MyThread::MyThread(QObject *parent)
{

}

void MyThread::run()
{
    qDebug()<<"myThread run() start to execute";
    qDebug()<<"     current thread ID:"<<QThread::currentThreadId()<<'\n';
    int count = 0;
    for(int i = 0;i!=1000000;++i)
    {
     ++count;
    }
    emit myThreadSignal(count);
    exec();
}

void MyThread::myThreadSlot(const int val)
{
    qDebug()<<"myThreadSlot() start to execute";
    qDebug()<<"     current thread ID:"<<QThread::currentThreadId()<<'\n';
    int count = 888;
    for(int i = 0;i!=1000000;++i)
    {
     ++count;
    }
}
#include "controller.h"
#include <mythread.h>
Controller::Controller(QObject *parent) : QObject(parent)
{
    myThrd = new MyThread;
    connect(myThrd,&MyThread::myThreadSignal,this,&Controller::handleResults);
    connect(myThrd, &QThread::finished, this, &QObject::deleteLater);            //該執行緒結束時銷燬
    connect(this,&Controller::operate,myThrd,&MyThread::myThreadSlot);

    myThrd->start();
    QThread::sleep(5);
    emit operate(999);
}

Controller::~Controller()
{
    myThrd->quit();
    myThrd->wait();
}

兩種方法的比較

兩種方法來執行執行緒都可以,隨便你的喜歡。不過看起來第二種更加簡單,容易讓人理解。不過我們的興趣在於這兩種使用方法到底有什麼區別?其最大的區別在於:

  • moveToThread方法,是把我們需要的工作全部封裝在一個類中,將每個任務定義為一個的槽函式,再建立觸發這些槽的訊號,然後把訊號和槽連線起來,最後將這個類呼叫moveToThread方法交給一個QThread物件,再呼叫QThread的start()函式使其全權處理事件迴圈。於是,任何時候我們需要讓執行緒執行某個任務,只需要發出對應的訊號就可以。其優點是我們可以在一個worker類中定義很多個需要做的工作,然後發出觸發的訊號執行緒就可以執行。相比於子類化的QThread只能執行run()函式中的任務,moveToThread的方法中一個執行緒可以做很多不同的工作(只要發出任務的對應的訊號即可)。 
  • 子類化QThread的方法,就是重寫了QThread中的run()函式,在run()函式中定義了需要的工作。這樣的結果是,我們自定義的子執行緒呼叫start()函式後,便開始執行run()函式。如果在自定義的執行緒類中定義相關槽函式,那麼這些槽函式不會由子類化的QThread自身事件迴圈所執行,而是由該子執行緒的擁有者所線上程(一般都是主執行緒)來執行。如果你不明白的話,請看,第二個例子中,子類化的執行緒的槽函式中輸出當前執行緒的ID,而這個ID居然是主執行緒的ID!!事實的確是如此,子類化的QThread只能執行run()函式中的任務直到run()函式退出,而它的槽函式根本不會被自己的執行緒執行。

QThread的訊號與槽

啟動或終止執行緒時,QThread提供了訊號與槽。

QThread的訊號
訊號 含義
void finished() 終止執行緒例項執行,傳送訊號
void started() 啟動執行緒例項,傳送訊號
void terminated() 結束執行緒例項,則傳送訊號
QThread的槽
含義
void quit() 執行緒終止執行槽
void start(Priority) 執行緒啟動槽
void terminate() 執行緒結束槽