Qt提供QThread類以進行多任務處理。與多任務處理一樣,Qt提供的線程可以做到單個線程做不到的事情。例如,網絡應用程序中,可以使用線程處理多種連接器。
QThread繼承自QObject類,且提供QMutex類以實現同步。線程和進程共享全局變量,可以使用互斥體對改變后的全局變量值實現同步。因此,必須編輯全局數據時,使用互斥體實現同步,其它進程則不能改變或瀏覽全局變量值。
什么是互斥體?
互斥體實現了“互相排斥”(mutual exclusion)同步的簡單形式(所以名為互斥體(mutex))。互斥體禁止多個線程同時進入受保護的代碼“臨界區”(critical section)。
在任意時刻,只有一個線程被允許進入代碼保護區。任何線程在進入臨界區之前,必須獲取(acquire)與此區域相關聯的互斥體的所有權。如果已有另一線程擁有了臨界區的互斥體,其他線程就不能再進入其中。這些線程必須等待,直到當前的屬主線程釋放(release)該互斥體。
什么時候需要使用互斥體呢?
互斥體用於保護共享的易變代碼,也就是,全局或靜態數據。這樣的數據必須通過互斥體進行保護,以防止它們在多個線程同時訪問時損壞。
1 class MyThread : public QThread 2 { 3 Q_OBJECT 4 protected: 5 void run(); 6 }; 7
8 void MyThread :: run(){ 9 ... 10 }
如上述代碼所示,如果要創建線程,則必須繼承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(),操作系統將根據優先級參數調度線程。如果線程已經在運行,那么這個函數什么也不做。優先級參數的效果取決於操作系統的調度策略。特別是那些不支持線程優先級的系統優先級將會被忽略。
線程執行
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()
請求線程的中斷。該請求是咨詢意見並且取決於線程上運行的代碼,來決定是否及如何執行這樣的請求。此函數不停止線程上運行的任何事件循環,並且在任何情況下都不會終止它。
線程等待
1 void msleep(unsigned long msecs) [static] //強制當前線程睡眠msecs毫秒
2
3 void sleep(unsigned long secs) [static] //強制當前線程睡眠secs秒
4
5 void usleep(unsigned long usecs) [static] //強制當前線程睡眠usecs微秒
6
7 bool wait(unsigned long time = ULONG_MAX) //線程將會被阻塞,等待time毫秒。和sleep不同的是,如果線程退出,wait會返回。
線程狀態
1 bool isFinished() const //線程是否結束
2
3 bool isRunning() const //線程是否正在運行
4
5 bool isInterruptionRequested() const //如果線程上的任務運行應該停止,返回true。可以使用requestInterruption()請求中斷。 6
7 //此函數可用於使長時間運行的任務干凈地中斷。從不檢查或作用於該函數返回值是安全的,但是建議在長時間運行的函數中經常這樣做。注意:不要過於頻繁調用,以保持較低的開銷。
線程優先級
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
方法描述:
定義一個繼承於QObject的worker類,在worker類中定義一個槽slot函數doWork(),這個函數中定義線程需要做的工作;
在要使用線程的controller類中,新建一個QThread的對象和woker類對象,使用moveToThread()方法將worker對象的事件循環全部交由QThread對象處理;
建立相關的信號函數和槽函數進行連接,然后發出信號觸發QThread的槽函數,使其執行工作。
例子:
1 #ifndef WORKER_H 2 #define WORKER_H
3 #include <QObject>
4 #include<QDebug>
5 #include<QThread>
6 class Worker:public QObject //work定義了線程要執行的工作
7 { 8 Q_OBJECT 9 public: 10 Worker(QObject* parent = nullptr){} 11 public slots: 12 void doWork(int parameter) //doWork定義了線程要執行的操作
13 { 14 qDebug()<<"receive the execute signal---------------------------------"; 15 qDebug()<<" current thread ID:"<<QThread::currentThreadId(); 16 for(int i = 0;i!=1000000;++i) 17 { 18 ++parameter; 19 } 20 qDebug()<<" finish the work and sent the resultReady signal\n"; 21 emit resultReady(parameter); //emit啥事也不干,是給程序員看的,表示發出信號發出信號
22 } 23
24 signals: 25 void resultReady(const int result); //線程完成工作時發送的信號
26 }; 27
28 #endif // WORKER_H
1 #ifndef CONTROLLER_H 2 #define CONTROLLER_H
3 #include <QObject>
4 #include<QThread>
5 #include<QDebug>
6 class Controller : public QObject //controller用於啟動線程和處理線程執行結果
7 { 8 Q_OBJECT 9 QThread workerThread; 10 public: 11 Controller(QObject *parent= nullptr); 12 ~Controller(); 13 public slots: 14 void handleResults(const int rslt) //處理線程執行的結果
15 { 16 qDebug()<<"receive the resultReady signal---------------------------------"; 17 qDebug()<<" current thread ID:"<<QThread::currentThreadId()<<'\n'; 18 qDebug()<<" the last result is:"<<rslt; 19 } 20 signals: 21 void operate(const int); //發送信號觸發線程
22 }; 23
24 #endif // CONTROLLER_H
1 #include "controller.h"
2 #include <worker.h>
3 Controller::Controller(QObject *parent) : QObject(parent) 4 { 5 Worker *worker = new Worker; 6 worker->moveToThread(&workerThread); //調用moveToThread將該任務交給workThread
7
8 connect(this, SIGNAL(operate(const int)), worker, SLOT(doWork(int))); //operate信號發射后啟動線程工作
9 connect(&workerThread, &QThread::finished, worker, &QObject::deleteLater); //該線程結束時銷毀
10 connect(worker, SIGNAL(resultReady(int)), this, SLOT(handleResults(int))); //線程結束后發送信號,對結果進行處理
11
12 workerThread.start(); //啟動線程
13 qDebug()<<"emit the signal to execute!---------------------------------"; 14 qDebug()<<" current thread ID:"<<QThread::currentThreadId()<<'\n'; 15 emit operate(0); 16 } 17
18 Controller::~Controller() //析構函數中調用quit()函數結束線程
19 { 20 workerThread.quit(); 21 workerThread.wait(); 22 }
繼承QThread類
方法描述
- 自定義一個繼承QThread的類MyThread,重載MyThread中的run()函數,在run()函數中寫入需要執行的工作;
- 調用start()函數來啟動線程。
例子:
1 #ifndef MYTHREAD_H 2 #define MYTHREAD_H
3 #include<QThread>
4 #include<QDebug>
5 class MyThread : public QThread 6 { 7 Q_OBJECT 8 public: 9 MyThread(QObject* parent = nullptr); 10 signals: //自定義發送的信號
11 void myThreadSignal(const int); 12 public slots: //自定義槽
13 void myThreadSlot(const int); 14 protected: 15 void run() override; 16 }; 17
18 #endif // MYTHREAD_H
1 #include "mythread.h"
2
3 MyThread::MyThread(QObject *parent) 4 { 5
6 } 7
8 void MyThread::run() 9 { 10 qDebug()<<"myThread run() start to execute"; 11 qDebug()<<" current thread ID:"<<QThread::currentThreadId()<<'\n'; 12 int count = 0; 13 for(int i = 0;i!=1000000;++i) 14 { 15 ++count; 16 } 17 emit myThreadSignal(count); 18 exec(); 19 } 20
21 void MyThread::myThreadSlot(const int val) 22 { 23 qDebug()<<"myThreadSlot() start to execute"; 24 qDebug()<<" current thread ID:"<<QThread::currentThreadId()<<'\n'; 25 int count = 888; 26 for(int i = 0;i!=1000000;++i) 27 { 28 ++count; 29 } 30 }
1 #include "controller.h"
2 #include <mythread.h>
3 Controller::Controller(QObject *parent) : QObject(parent) 4 { 5 myThrd = new MyThread; 6 connect(myThrd,&MyThread::myThreadSignal,this,&Controller::handleResults); 7 connect(myThrd, &QThread::finished, this, &QObject::deleteLater); //該線程結束時銷毀
8 connect(this,&Controller::operate,myThrd,&MyThread::myThreadSlot); 9
10 myThrd->start(); 11 QThread::sleep(5); 12 emit operate(999); 13 } 14
15 Controller::~Controller() 16 { 17 myThrd->quit(); 18 myThrd->wait(); 19 }
兩種方法的比較
兩種方法來執行線程都可以,隨便你的喜歡。不過看起來第二種更加簡單,容易讓人理解。不過我們的興趣在於這兩種使用方法到底有什么區別?其最大的區別在於:
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() 線程結束槽