【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提供的執行緒類
執行緒類 | 說明 |
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類。主執行緒等待與其他執行緒的中斷時,必須進行同步。例如:兩個執行緒同時訪問共享變數,那麼可能得不到預想的結果。因此,兩個執行緒訪問共享變數時,必須進行同步。
- 一個執行緒訪問指定的共享變數時,為了禁止其他執行緒訪問,QMutex提供了類似鎖定裝置的功能。互斥體啟用狀態下,執行緒不能同時訪問共享變數,必須在先訪問的執行緒完成訪問後,其他執行緒才可以繼續訪問。
- 一個執行緒訪問互斥體鎖定的共享變數期間,如果其他執行緒也訪問此共享變數,那麼該執行緒將會一直處於休眠狀態,直到正在訪問的執行緒結束訪問。這稱為執行緒安全。
- QReadWriteLock和QMutex的功能相同,區別在於,QReadWriteLock對資料的訪問分為讀訪問和寫訪問。很多執行緒頻繁訪問共享變數時,與QMetex相對,使用QReadWriteLock更合適。
- QSemaphore擁有和QMutex一樣的同步功能,可以管理多個按數字識別的資源。QMutex只能管理一個資源,但如果使用QSemaphore,則可以管理多個按號碼識別的資源。
- 條件符合時,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的任何值。
常量 | 值 | 優先順序 |
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
方法描述:
- 定義一個繼承於QObject的worker類,在worker類中定義一個槽slot函式doWork(),這個函式中定義執行緒需要做的工作;
- 在要使用執行緒的controller類中,新建一個QThread的物件和woker類物件,使用moveToThread()方法將worker物件的事件迴圈全部交由QThread物件處理;
- 建立相關的訊號函式和槽函式進行連線,然後發出訊號觸發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提供了訊號與槽。
訊號 | 含義 |
void finished() | 終止執行緒例項執行,傳送訊號 |
void started() | 啟動執行緒例項,傳送訊號 |
void terminated() | 結束執行緒例項,則傳送訊號 |
槽 | 含義 |
void quit() | 執行緒終止執行槽 |
void start(Priority) | 執行緒啟動槽 |
void terminate() | 執行緒結束槽 |