深入理解QT多線程編程
一、線程基礎
1、GUI線程與工作線程
每個程序啟動後擁有的第一個線程稱為主線程,即GUI線程。QT中所有的組件類和幾個相關的類隻能工作在GUI線程,不能工作在次線程,次線程即工作線程,主要負責處理GUI線程卸下的工作。
2、數據的同步訪問
每個線程都有自己的棧,因此每個線程都要自己的調用歷史和本地變量。線程共享相同的地址空間。
二、QT多線程簡介
QT通過三種形式提供瞭對線程的支持,分別是平臺無關的線程類、線程安全的事件投遞、跨線程的信號-槽連接。
QT中線程類包含如下:
- QThread 提供瞭跨平臺的多線程解決方案
- QThreadStorage 提供逐線程數據存儲
- QMutex 提供相互排斥的鎖,或互斥量
- QMutexLocker 是一個輔助類,自動對 QMutex 加鎖與解鎖
- QReadWriterLock 提供瞭一個可以同時讀操作的鎖
- QReadLocker與QWriteLocker 自動對QReadWriteLock 加鎖與解鎖
- QSemaphore 提供瞭一個整型信號量,是互斥量的泛化
- QWaitCondition 提供瞭一種方法,使得線程可以在被另外線程喚醒之前一直休眠。
三、QThread線程
1、QThread線程基礎
QThread是Qt線程中有一個公共的抽象類,所有的線程類都是從QThread抽象類中派生的,需要實現QThread中的虛函數run(),通過start()函數來調用run函數。
void run()函數是線程體函數,用於定義線程的功能。
void start()函數是啟動函數,用於將線程入口地址設置為run函數。
void terminate()函數用於強制結束線程,不保證數據完整性和資源釋放。
QCoreApplication::exec()總是在主線程(執行main()的線程)中被調用,不能從一個QThread中調用。在GUI程序中,主線程也稱為GUI線程,是唯一允許執行GUI相關操作的線程。另外,必須在創建一個QThread前創建QApplication(or QCoreApplication)對象。
當線程啟動和結束時,QThread會發送信號started()和finished(),可以使用isFinished()和isRunning()來查詢線程的狀態。
從Qt4.8起,可以釋放運行剛剛結束的線程對象,通過連接finished()信號到QObject::deleteLater()槽。
使用wait()來阻塞調用的線程,直到其它線程執行完畢(或者直到指定的時間過去)。
靜態函數currentThreadId()和currentThread()返回標識當前正在執行的線程。前者返回線程的ID,後者返回一個線程指針。
要設置線程的名稱,可以在啟動線程之前調用setObjectName()。如果不調用setObjectName(),線程的名稱將是線程對象的運行時類型(QThread子類的類名)。
2、線程的優先級
QThread線程總共有8個優先級
QThread::IdlePriority 0 scheduled only when no other threads are running. QThread::LowestPriority 1 scheduled less often than LowPriority. QThread::LowPriority 2 scheduled less often than NormalPriority. QThread::NormalPriority 3 the default priority of the operating system. QThread::HighPriority 4 scheduled more often than NormalPriority. QThread::HighestPriority 5 scheduled more often than HighPriority. QThread::TimeCriticalPriority 6 scheduled as often as possible. QThread::InheritPriority 7 use the same priority as the creating thread. This is the default. void setPriority(Priority priority)
設置正在運行線程的優先級。如果線程沒有運行,此函數不執行任何操作並立即返回。使用的start()來啟動一個線程具有特定的優先級。優先級參數可以是QThread::Priority枚舉除InheritPriortyd的任何值。
3、線程的創建
void start ( Priority priority = InheritPriority )
啟動線程執行,啟動後會發出started ()信號
4、線程的執行
int exec() [protected]
進入事件循環並等待直到調用exit(),返回值是通過調用exit()來獲得,如果調用成功則返回0。
void run() [virtual protected]
線程的起點,在調用start()之後,新創建的線程就會調用run函數,默認實現調用exec(),大多數需要重新實現run函數,便於管理自己的線程。run函數返回時,線程的執行將結束。
5、線程的退出
void quit();
通知線程事件循環退出,返回0表示成功,相當於調用瞭QThread::exit(0)。
void exit ( int returnCode = 0 );
調用exit後,thread將退出event loop,並從exec返回,exec的返回值就是returnCode。通常returnCode=0表示成功,其他值表示失敗。
void terminate ();
結束線程,線程是否立即終止取決於操作系統。
線程被終止時,所有等待該線程Finished的線程都將被喚醒。
terminate是否調用取決於setTerminationEnabled ( bool enabled = true )開關。
void requestInterruption()
請求線程的中斷。請求是咨詢意見並且取決於線程上運行的代碼,來決定是否及如何執行這樣的請求。此函數不停止線程上運行的任何事件循環,並且在任何情況下都不會終止它。
工程中線程退出的解決方案如下:
通過在線程類中增加標識變量volatile bool m_stop,通過m_stop變量的值判斷run函數是否執行結束返回。
#ifndef WORKTHREAD_H #define WORKTHREAD_H #include <QThread> #include <QDebug> class WorkThread : public QThread { protected: //線程退出的標識量 volatile bool m_stop; void run() { qDebug() << "run begin"; while(!m_stop) { //task handling int* p = new int[1000]; for(int i = 0; i < 1000; i++) { p[i] = i * i; } sleep(2); delete [] p; } qDebug() << "run end"; } public: WorkThread() m_stop = false; //線程退出的接口函數,用戶使用 void stop() m_stop = true; }; #endif // WORKTHREAD_H
6、線程的等待
bool wait ( unsigned long time = ULONG_MAX )
線程將會被阻塞,等待time毫秒,如果線程退出,則wait會返回。Wait函數解決多線程在執行時序上的依賴。
void msleep ( unsigned long msecs ) void sleep ( unsigned long secs ) void usleep ( unsigned long usecs )
sleep()、msleep()、usleep()允許秒,毫秒和微秒來區分,但在Qt5.0中被設為public。
一般情況下,wait()和sleep()函數應該不需要,因為Qt是一個事件驅動型框架。考慮監聽finished()信號來取代wait(),使用QTimer來取代sleep()。
7、線程的狀態
bool isFinished () const 線程是否已經退出
bool isRunning () const 線程是否處於運行狀態
8、線程的屬性
Priority priority () const void setPriority ( Priority priority ) uint stackSize () const void setStackSize ( uint stackSize ) void setTerminationEnabled ( bool enabled = true )
設置是否響應terminate()函數
9、線程與事件循環
QThread中run()的默認實現調用瞭exec(),從而創建一個QEventLoop對象,由QEventLoop對象處理線程中事件隊列(每一個線程都有一個屬於自己的事件隊列)中的事件。exec()在其內部不斷做著循環遍歷事件隊列的工作,調用QThread的quit()或exit()方法使退出線程,盡量不要使用terminate()退出線程,terminate()退出線程過於粗暴,造成資源不能釋放,甚至互斥鎖還處於加鎖狀態。
線程中的事件循環,使得線程可以使用那些需要事件循環的非GUI 類(如,QTimer,QTcpSocket,QProcess)。
在QApplication前創建的對象,QObject::thread()返回NULL,意味著主線程僅為這些對象處理投遞事件,不會為沒有所屬線程的對象處理另外的事件。可以用QObject::moveToThread()來改變對象及其子對象的線程親緣關系,假如對象有父親,不能移動這種關系。在另一個線程(而不是創建它的線程)中delete QObject對象是不安全的。除非可以保證在同一時刻對象不在處理事件。可以用QObject::deleteLater(),它會投遞一個DeferredDelete事件,這會被對象線程的事件循環最終選取到。假如沒有事件循環運行,事件不會分發給對象。假如在一個線程中創建瞭一個QTimer對象,但從沒有調用過exec(),那麼QTimer就不會發射它的timeout()信號,deleteLater()也不會工作。可以手工使用線程安全的函數QCoreApplication::postEvent(),在任何時候,給任何線程中的任何對象投遞一個事件,事件會在那個創建瞭對象的線程中通過事件循環派發。事件過濾器在所有線程中也被支持,不過它限定被監視對象與監視對象生存在同一線程中。QCoreApplication::sendEvent(不是postEvent()),僅用於在調用此函數的線程中向目標對象投遞事件。
四、線程的同步
1、線程同步基礎
臨界資源:每次隻允許一個線程進行訪問的資源
線程間互斥:多個線程在同一時刻都需要訪問臨界資源
線程鎖能夠保證臨界資源的安全性,通常,每個臨界資源需要一個線程鎖進行保護。
線程死鎖:線程間相互等待臨界資源而造成彼此無法繼續執行。
產生死鎖的條件:
A、系統中存在多個臨界資源且臨界資源不可搶占
B、線程需要多個臨界資源才能繼續執行
死鎖的避免:
A、對使用的每個臨界資源都分配一個唯一的序號
B、對每個臨界資源對應的線程鎖分配相應的序號
C、系統中的每個線程按照嚴格遞增的次序請求臨界資源
QMutex, QReadWriteLock, QSemaphore, QWaitCondition 提供瞭線程同步的手段。使用線程的主要想法是希望它們可以盡可能並發執行,而一些關鍵點上線程之間需要停止或等待。例如,假如兩個線程試圖同時訪問同一個全局變量,結果可能不如所願。
2、互斥量QMutex
QMutex 提供相互排斥的鎖,或互斥量。在一個時刻至多一個線程擁有mutex,假如一個線程試圖訪問已經被鎖定的mutex,那麼線程將休眠,直到擁有mutex的線程對此mutex解鎖。QMutex常用來保護共享數據訪問。QMutex類所以成員函數是線程安全的。
頭文件聲明: #include <QMutex>
互斥量聲明: QMutex m_Mutex;
互斥量加鎖: m_Mutex.lock();
互斥量解鎖: m_Mutex.unlock();
如果對沒有加鎖的互斥量進行解鎖,結果是未定義的。互斥量的加鎖和解鎖必須在同一線程中成對出現。
QMutex ( RecursionMode mode = NonRecursive )
QMutex有兩種模式:Recursive, NonRecursive
A、Recursive
一個線程可以對mutex多次lock,直到相應次數的unlock調用後,mutex才真正被解鎖。
B、NonRecursive
默認模式,mutex隻能被lock一次。
如果使用瞭Mutex.lock()而沒有對應的使用Mutex.unlcok()的話就會造成死鎖,其他的線程將永遠也得不到接觸Mutex鎖住的共享資源的機會。盡管可以不使用lock()而使用tryLock(timeout)來避免因為死等而造成的死鎖( tryLock(負值)==lock()),但是還是很有可能造成錯誤。
bool tryLock();
如果當前其他線程已對該mutex加鎖,則該調用會立即返回,而不被阻塞。
bool tryLock(int timeout);
如果當前其他線程已對該mutex加鎖,則該調用會等待一段時間,直到超時
QMutex mutex; int complexFunction(int flag) { mutex.lock(); int retVal = 0; switch (flag) { case 0: case 1: mutex.unlock(); return moreComplexFunction(flag); case 2: { int status = anotherFunction(); if (status < 0) { mutex.unlock(); return -2; } retVal = status + flag; } break; default: if (flag > 10) { mutex.unlock(); return -1; } mutex.unlock(); return retVal; }
3、互斥鎖QMutexLocker
在較復雜的函數和異常處理中對QMutex類mutex對象進行lock()和unlock()操作將會很復雜,進入點要lock(),在所有跳出點都要unlock(),很容易出現在某些跳出點未調用unlock(),所以Qt引進瞭QMutex的輔助類QMutexLocker來避免lock()和unlock()操作。在函數需要的地方建立QMutexLocker對象,並把mutex指針傳給QMutexLocker對象,此時mutex已經加鎖,等到退出函數後,QMutexLocker對象局部變量會自己銷毀,此時mutex解鎖。
頭文件聲明: #include<QMutexLocker>
互斥鎖聲明: QMutexLocker mutexLocker(&m_Mutex);
互斥鎖加鎖: 從聲明處開始(在構造函數中加鎖)
互斥鎖解鎖: 出瞭作用域自動解鎖(在析構函數中解鎖)
int complexFunction(int flag) QMutexLocker locker(&mutex); if (status < 0) if (flag > 10)
4、QReadWriteLock
QReadWriterLock 與QMutex相似,但對讀寫操作訪問進行區別對待,可以允許多個讀者同時讀數據,但隻能有一個寫,並且寫讀操作不同同時進行。使用QReadWriteLock而不是QMutex,可以使得多線程程序更具有並發性。 QReadWriterLock默認模式是NonRecursive。
QReadWriterLock類成員函數如下:
QReadWriteLock ( ) QReadWriteLock ( RecursionMode recursionMode ) void lockForRead () void lockForWrite () bool tryLockForRead () bool tryLockForRead ( int timeout ) bool tryLockForWrite () bool tryLockForWrite ( int timeout ) boid unlock () 使用實例: QReadWriteLock lock; void ReaderThread::run() lock.lockForRead(); read_file(); lock.unlock(); void WriterThread::run() lock.lockForWrite(); write_file();
5、QReadLocker和QWriteLocker
在較復雜的函數和異常處理中對QReadWriterLock類lock對象進行lockForRead()/lockForWrite()和unlock()操作將會很復雜,進入點要lockForRead()/lockForWrite(),在所有跳出點都要unlock(),很容易出現在某些跳出點未調用unlock(),所以Qt引進瞭QReadLocker和QWriteLocker類來簡化解鎖操作。在函數需要的地方建立QReadLocker或QWriteLocker對象,並把lock指針傳給QReadLocker或QWriteLocker對象,此時lock已經加鎖,等到退出函數後,QReadLocker或QWriteLocker對象局部變量會自己銷毀,此時lock解鎖。
QByteArray readData() ... return data;
使用QReadLocker:
QReadLocker locker(&lock);
6、信號量QSemaphore
QSemaphore 是QMutex的一般化,是特殊的線程鎖,允許多個線程同時訪問臨界資源,而一個QMutex隻保護一個臨界資源。QSemaphore 類的所有成員函數是線程安全的。
經典的生產者-消費者模型如下:某工廠隻有固定倉位,生產人員每天生產的產品數量不一,銷售人員每天銷售的產品數量也不一致。當生產人員生產P個產品時,就一次需要P個倉位,當銷售人員銷售C個產品時,就要求倉庫中有足夠多的產品才能銷售。如果剩餘倉位沒有P個時,該批次的產品都不存入,當當前已有的產品沒有C個時,就不能銷售C個以上的產品,直到新產品加入後方可銷售。
QSemaphore來控制對環狀緩沖的訪問,此緩沖區被生產者線程和消費者線程共享。生產者不斷向緩沖區寫入數據直到緩沖末端,再從頭開始。消費者從緩沖不斷讀取數據。信號量比互斥量有更好的並發性,假如我們用互斥量來控制對緩沖的訪問,那麼生產者、消費者不能同時訪問緩沖區。然而,我們知道在同一時刻,不同線程訪問緩沖的不同部分並沒有什麼危害。
QSemaphore 類成員函數:
QSemaphore ( int n = 0 ) void acquire ( int n = 1 ) int available () const void release ( int n = 1 ) bool tryAcquire ( int n = 1 ) bool tryAcquire ( int n, int timeout )
實例代碼:
QSemaphore sem(5); // sem.available() == 5 sem.acquire(3); // sem.available() == 2 sem.acquire(2); // sem.available() == 0 sem.release(5); // sem.available() == 5 sem.release(5); // sem.available() == 10 sem.tryAcquire(1); // sem.available() == 9, returns true sem.tryAcquire(250); // sem.available() == 9, returns false
生產者-消費者實例:
#include <QtCore/QCoreApplication> #include <QSemaphore> #include <cstdlib> #include <cstdio> const int DataSize = 100000; const int BufferSize = 8192; char buffer[BufferSize]; QSemaphore production(BufferSize); QSemaphore consumption; class Producor:public QThread void run(); void Producor::run() for(int i = 0; i < DataSize; i++) production.acquire(); buffer[i%BufferSize] = "ACGT"[(int)qrand()%4]; consumption.release(); } class Consumer:public QThread void Consumer::run() consumption.acquire(); fprintf(stderr, "%c", buffer[i%BufferSize]); production.release(); fprintf(stderr, "%c", "\n"); int main(int argc, char *argv[]) QCoreApplication a(argc, argv); Producor productor; Consumer consumer; productor.start(); consumer.start(); productor.wait(); consumer.wait(); return a.exec();
Producer::run函數:
當producer線程執行run函數,如果buffer中已滿,而consumer線程沒有讀,producer不能再往buffer中寫字符,在 productor.acquire 處阻塞直到 consumer線程讀(consume)數據。一旦producer獲取到一個字節(資源)就寫入一個隨機的字符,並調用 consumer.release 使consumer線程可以獲取一個資源(讀一個字節的數據)。
Consumer::run函數:
當consumer線程執行run函數,如果buffer中沒有數據,則consumer線程在consumer.acquire處阻塞,直到producer線程執行寫操作寫入一個字節,並執行consumer.release 使consumer線程的可用資源數=1時,consumer線程從阻塞狀態中退出, 並將consumer 資源數-1,consumer當前資源數=0。
7、等待條件QWaitCondition
QWaitCondition 允許線程在某些情況發生時喚醒另外的線程。一個或多個線程可以阻塞等待QWaitCondition ,用wakeOne()或wakeAll()設置一個條件。wakeOne()隨機喚醒一個,wakeAll()喚醒所有。
QWaitCondition () bool wait ( QMutex * mutex, unsigned long time = ULONG_MAX ) bool wait ( QReadWriteLock * readWriteLock, unsigned long time = ULONG_MAX ) void wakeOne () void wakeAll ()
頭文件聲明: #include <QWaitCondition>
等待條件聲明: QWaitCondtion m_WaitCondition;
等待條件等待: m_WaitConditon.wait(&m_muxtex, time);
等待條件喚醒: m_WaitCondition.wakeAll();
在經典的生產者-消費者場合中,生產者首先必須檢查緩沖是否已滿(numUsedBytes==BufferSize),如果緩沖區已滿,線程停下來等待 bufferNotFull條件。如果沒有滿,在緩沖中生產數據,增加numUsedBytes,激活條件 bufferNotEmpty。使用mutex來保護對numUsedBytes的訪問。QWaitCondition::wait() 接收一個mutex作為參數,mutex被調用線程初始化為鎖定狀態。在線程進入休眠狀態之前,mutex會被解鎖。而當線程被喚醒時,mutex會處於鎖定狀態,從鎖定狀態到等待狀態的轉換是原子操作。當程序開始運行時,隻有生產者可以工作,消費者被阻塞等待bufferNotEmpty條件,一旦生產者在緩沖中放入一個字節,bufferNotEmpty條件被激發,消費者線程於是被喚醒。
#include <QWaitCondition> #include <QMutex> #include <QTime> const int DataSize = 32; const int BufferSize = 16; QWaitCondition bufferNotEmpty; QWaitCondition bufferNotFull; int used = 0; qsrand(QTime(0,0,0).secsTo(QTime::currentTime())); mutex.lock(); if(used == BufferSize) bufferNotFull.wait(&mutex); mutex.unlock(); buffer[i%BufferSize] = used; used++; bufferNotEmpty.wakeAll(); if(used == 0) bufferNotEmpty.wait(&mutex); fprintf(stderr, "%d\n", buffer[i%BufferSize]); used--; bufferNotFull.wakeAll();
8、高級事件隊列
QT事件系統對進程間通信很重要,每個進程可以有自己的事件循環,要在另外一個線程中調用一個槽函數(或任何invokable方法),需要將調用槽函數放置在目標線程的事件循環中,讓目標線程在槽函數開始運行之前,先完成自己的當前任務,而原來的線程繼續並行運行。
要在一個事件循環中執行調用槽函數,需要一個queued信號槽連接。每當信號發出時,信號的參數將被事件系統記錄。信號接收者存活的線程將運行槽函數。另外,不使用信號,調用QMetaObject::invokeMethod()也可以達到相同的效果。在這兩種情況下,必須使用queued連接,因為direct連接繞過瞭事件系統,並且立即在當前線程中運行此方法。
當線程同步使用事件系統時,沒有死鎖風險。然而,事件系統不執行互斥。如果調用方法訪問共享數據,仍然需要使用QMutex來保護。
如果隻使用信號槽,並且線程間沒有共享變量,那麼,多線程程序可以完全沒有低級原語。
五、可重入與線程安全
可重入reentrant與線程安全thread-safe被用來說明一個函數如何用於多線程程序。
一個線程安全的函數可以同時被多個線程調用,甚至調用者會使用共享數據也沒有問題,因為對共享數據的訪問是串行的。一個可重入函數也可以同時被多個線程調用,但是每個調用者隻能使用自己的數據。因此,一個線程安全的函數總是可重入的,但一個可重入的函數並不一定是線程安全的。
一個可重入的類,指的是類的成員函數可以被多個線程安全地調用,隻要每個線程使用類的不同的對象。而一個線程安全的類,指的是類的成員函數能夠被多線程安全地調用,即使所有的線程都使用類的同一個實例。
1、可重入
大多數C++類是可重入的,因為它們典型地僅僅引用成員數據。任何線程可以訪問可重入類實例的成員函數,隻要同一時間沒有其他線程調用這個實例的成員函數。
class Counter public: Counter() {n=0;} void increment() {++n;} void decrement() {--n;} int value() const {return n;} private: int n;
Counter類是可重入的,但卻不是線程安全的。假如多個線程都試圖修改數據成員n,結果未定義。
大多數Qt類是可重入,非線程安全的。有一些類與函數是線程安全的,主要是線程相關的類,如QMutex,QCoreApplication::postEvent()。
2、線程安全
所有的GUI類(如QWidget及其子類),操作系統核心類(如QProcess)和網絡類都不是線程安全的。
public: Counter() { n = 0; } void increment() { QMutexLocker locker(&mutex); ++n; } void decrement() { QMutexLocker locker(&mutex); --n; } int value() const { QMutexLocker locker(&mutex); return n; } private: mutable QMutex mutex; int n; };
Counter類是可重入和線程安全的。QMutexLocker類在構造函數中自動對mutex進行加鎖,在析構函數中進行解鎖。mutex使用瞭mutable關鍵字來修飾,因為在value()函數中對mutex進行加鎖與解鎖操作,而value()是一個const函數。
六、線程與信號槽
1、線程的依附性
線程的依附性是對象與線程的關系。默認情況下,對象依附於自身被創建的線程。
對象的依附性與槽函數執行的關系,默認情況下,槽函數在其所依附的線程中被調用執行。
修改對象的依附性的方法:QObject::moveToThread函數用於改變對象的線程依附性,使得對象的槽函數在依附的線程中被調用執行。
2、QObject與線程
QThread類具有發送信號和定義槽函數的能力。QThread主要信號如下:
- void started();線程開始運行時發送信號
- void finished();線程完成運行時發送信號
- void terminated();線程被異常終止時發送信號
QThread繼承自QObject,發射信號以指示線程執行開始與結束,並提供瞭許多槽函數。QObjects可以用於多線程,發射信號以在其它線程中調用槽函數,並且向“存活”於其它線程中的對象發送事件。
QObject的可重入性
QObject是可重入的,QObject的大多數非GUI子類如 QTimer、QTcpSocket、QUdpSocket、QHttp、QFtp、QProcess也是可重入的,在多個線程中同時使用這些類是可能的。可重入的類被設計成在一個單線程中創建與使用,在一個線程中創建一個對象而在另一個線程中調用該對象的函數,不保證能行得通。有三種約束需要註意:
A、一個QObject類型的孩子必須總是被創建在它的父親所被創建的線程中。這意味著,除瞭別的以外,永遠不要把QThread對象(this)作為該線程中創建的一個對象的父親(因為QThread對象自身被創建在另外一個線程中)。
B、事件驅動的對象可能隻能被用在一個單線程中。特別適用於計時器機制(timer mechanism)和網絡模塊。例如:不能在不屬於這個對象的線程中啟動一個定時器或連接一個socket,必須保證在刪除QThread之前刪除所有創建在這個線程中的對象。在run()函數的實現中,通過在棧中創建這些對象,可以輕松地做到這一點。
C、雖然QObject是可重入的,但GUI類,尤其是QWidget及其所有子類都不是可重入的,隻能被用在GUI線程中。QCoreApplication::exec()必須也從GUI線程被調用。
在實踐中,隻能在主線程而非其它線程中使用GUI的類,可以很輕易地被解決:將耗時操作放在一個單獨的工作線程中,當工作線程結束後在GUI線程中由屏幕顯示結果。
一般來說,在QApplication前創建QObject是不行的,會導致奇怪的崩潰或退出,取決於平臺。因此,不支持QObject的靜態實例。一個單線程或多線程的應用程序應該先創建QApplication,並最後銷毀QObject。
3、線程的事件循環
每個線程都有自己的事件循環。主線程通過QCoreApplication::exec()來啟動自己的事件循環,但對話框的GUI應用程序,有些時候用QDialog::exec(),其它線程可以用QThread::exec()來啟動事件循環。就像 QCoreApplication,QThread提供一個exit(int)函數和quit()槽函數。
線程中的事件循環使得線程可以利用一些非GUI的、要求有事件循環存在的Qt類(例如:QTimer、QTcpSocket、和QProcess),使得連接一些線程的信號到一個特定線程的槽函數成為可能。
一個QObject實例被稱為存活於它所被創建的線程中。關於這個對象的事件被分發到該線程的事件循環中。可以用QObject::thread()方法獲取一個QObject所處的線程。
QObject::moveToThread()函數改變一個對象和及其子對象的線程所屬性。(如果對象有父對象的話,對象不能被移動到其它線程中)。
從另一個線程(不是QObject對象所屬的線程)對該QObject對象調用delete方法是不安全的,除非能保證該對象在那個時刻不處理事件,使用QObejct::deleteLater()更好。一個DeferredDelete類型的事件將被提交(posted),而該對象的線程的 件循環最終會處理這個事件。默認情況下,擁有一個QObject的線程就是創建QObject的線程,而不是 QObject::moveToThread()被調用後的。
如果沒有事件循環運行,事件將不會傳遞給對象。例如:在一個線程中創建瞭一個QTimer對象,但從沒有調用exec(),那麼,QTimer就永遠不會發射timeout()信號,即使調用deleteLater()也不行。(這些限制也同樣適用於主線程)。
利用線程安全的方法QCoreApplication::postEvent(),可以在任何時刻給任何線程中的任何對象發送事件,事件將自動被分發到該對象所被創建的線程事件循環中。
所有的線程都支持事件過濾器,而限制是監控對象必須和被監控對象存在於相同的線程中。QCoreApplication::sendEvent()(不同於postEvent())隻能將事件分發到和該函數調用者相同的線程中的對象。
4、其他線程訪問QObject子類
QObject及其所有子類都不是線程安全的。這包含瞭整個事件交付系統。重要的是,切記事件循環可能正在向你的QObject子類發送事件,當你從另一個線程訪問該對象時。
如果你正在調用一個QObject子類的函數,而該子類對象並不存活於當前線程中,並且該對象是可以接收事件的,那麼你必須用一個mutex保護對該QObject子類的內部數據的所有訪問,否則,就有可能發生崩潰和非預期的行為。
同其它對象一樣,QThread對象存活於該對象被創建的線程中 – 而並非是在QThread::run()被調用時所在的線程。一般來說,在QThread子類中提供槽函數是不安全的,除非用一個mutex保護成員變量。
另一方面,可以在QThread::run()的實現中安全地發射信號,因為信號發射是線程安全的。
5、跨線程的信號槽
線程的信號槽機制需要開啟線程的事件循環機制,即調用QThread::exec()函數開啟線程的事件循環。
Qt信號-槽連接函數原型如下:
bool QObject::connect ( const QObject * sender, const char * signal, const QObject * receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection )
Qt支持5種連接方式
A、Qt::DirectConnection(直連方式)(信號與槽函數關系類似於函數調用,同步執行)
當信號發出後,相應的槽函數將立即被調用。emit語句後的代碼將在所有槽函數執行完畢後被執行。
當信號發射時,槽函數將直接被調用。
無論槽函數所屬對象在哪個線程,槽函數都在發射信號的線程內執行。
B、Qt::QueuedConnection(隊列方式)(此時信號被塞到事件隊列裡,信號與槽函數關系類似於消息通信,異步執行)
當信號發出後,排隊到信號隊列中,需等到接收對象所屬線程的事件循環取得控制權時才取得該信號,調用相應的槽函數。emit語句後的代碼將在發出信號後立即被執行,無需等待槽函數執行完畢。
當控制權回到接收者所依附線程的事件循環時,槽函數被調用。
槽函數在接收者所依附線程執行。
C、Qt::AutoConnection(自動方式)
Qt的默認連接方式,如果信號的發出和接收信號的對象同屬一個線程,那個工作方式與直連方式相同;否則工作方式與隊列方式相同。
如果信號在接收者所依附的線程內發射,則等同於直接連接
如果發射信號的線程和接受者所依附的線程不同,則等同於隊列連接
D、Qt::BlockingQueuedConnection(信號和槽必須在不同的線程中,否則就產生死鎖)
槽函數的調用情形和Queued Connection相同,不同的是當前的線程會阻塞住,直到槽函數返回。
E、Qt::UniqueConnection
與默認工作方式相同,隻是不能重復連接相同的信號和槽,因為如果重復連接就會導致一個信號發出,對應槽函數就會執行多次。
QThread是用來管理線程的,QThread對象所依附的線程和所管理的線程並不是同一個概念。QThread所依附的線程,就是創建QThread對象的線程,QThread 所管理的線程,就是run啟動的線程,也就是新建線程。QThread對象依附在主線程中,QThread對象的slot函數會在主線程中執行,而不是次線程。除非QThread對象依附到次線程中(通過movetoThread)。
工程實踐中,為瞭避免凍結主線程的事件循環(即避免因此而凍結瞭應用的UI),所有的計算工作是在一個單獨的工作線程中完成的,工作線程結束時發射一個信號,通過信號的參數將工作線程的狀態發送到GUI線程的槽函數中更新GUI組件狀態。
七、線程的設計
1、線程的生命周期
如果線程的正處於執行過程中時,線程對象被銷毀時,程序將會出錯。
工程實踐中線程對象的生命期必須大於線程的生命期。
2、同步線程類設計
線程對象主動等待線程生命期結束後才銷毀,線程對象銷毀時確保線程執行結束,支持在棧或堆上創建線程對象。
在線程類的析構函數中先調用wait函數,強制等待線程執行結束。
使用場合:適用於線程生命期較短的場合
#ifndef SYNCTHREAD_H #define SYNCTHREAD_H class SyncThread : public QThread Q_OBJECT explicit SyncThread(QObject* parent = 0):QThread(parent) ~SyncThread() wait(); #endif // SYNCTHREAD_H
3、異步線程類設計
線程生命期結束時通知線程對象銷毀。
隻能在堆空間創建線程對象,線程對象不能被外界主動銷毀。
在run函數中最後調用deleteLater()函數。
線程函數主動申請銷毀線程對象。
使用場合:
線程生命期不可控,需要長時間運行於後臺的線程。
#ifndef ASYNCTHREAD_H #define ASYNCTHREAD_H class AsyncThread : public QThread deleteLater(); explicit AsyncThread(QObject* parent = 0):QThread(parent) ~AsyncThread() static AsyncThread* newThread(QObject* parent = 0) return new AsyncThread(parent); #endif // ASYNCTHREAD_H
八、線程的使用方式
1、子類化QThread
QThread的兩種使用方法:
(1)不使用事件循環
A、子類化 QThread
B、重寫run函數,run函數內有一個 while 或 for 的死循環
C、設置一個標記為來控制死循環的退出。
適用於後臺執行長時間的耗時操作,如文件復制、網絡數據讀取。
(2)使用事件循環。
A、子類化 QThread
B、重寫run 使其調用 QThread::exec() ,開啟線程的事件循環
C、為子類定義信號和槽,由於槽函數並不會在新開的 Thread 運行,在構造函數中調用 moveToThread(this)。
適用於事務性操作,如文件讀寫、數據庫讀寫。
2、Worker-Object
在Qt4.4之前,run 是純虛函數,必須子類化QThread來實現run函數。
而從Qt4.4開始,QThread不再支持抽象類,run 默認調用 QThread::exec() ,不需要子類化 QThread,隻需要子類化一個 QObject 。
通過繼承的方式實現多線程已經沒有任何意義,QThread是操作系統線程的接口或控制點,用於充當線程操作的集合。
使用Worker-Object通過QObject::moveToThread將它們移動到線程中。
指定一個線程對象的線程入口函數的方法:
A、在類中定義一個槽函數void tmain()作為線程入口函數
B、在類中定義一個QThread成員對象m_thread
C、改變當前對象的線程依附性到m_thread
D、連接m_thread的started()信號到tmain槽函數。
#ifndef WORKER_H #define WORKER_H #include <QObject> class Worker : public QObject QThread m_thread; protected slots: void tmain() qDebug() << "void tmain()"; explicit Worker(QObject* parent = 0):QObject(parent) moveToThread(&m_thread); connect(&m_thread, SIGNAL(started()), this, SLOT(tmain())); void start() m_thread.start(); void terminate() m_thread.terminate(); void exit(int c) m_thread.exit(c); ~Worker() m_thread.wait(); #endif // WORKER_H
九、多線程與GUI組件的通信
1、多線程與GUI組件通信基礎
GUI系統的設計原則:
所有界面組件的創建隻能在GUI線程(主線程)中完成。子線程與界面組件的通信有兩種方式:
A、信號槽方式
B、發送自定事件方式
2、信號槽方式
使用信號槽解決多線程與界面組件的通信的方案:
A、在子線程中定義界面組件的更新信號
B、在主窗口類中定義更新界面組件的槽函數
C、使用異步方式連接更新信號到槽函數
子線程通過發送信號的方式更新界面組件,所有的界面組件對象隻能依附於GUI線程(主線程)。
子線程更新界面狀態的本質是子線程發送信號通知主線程界面更新請求,主線程根據具體信號以及信號參數對界面組件進行修改。
使用信號槽在子線程中更新主界面中進度條的進度顯示信息。
工作線程類:
signals: void signalProgressValue(int value); work(); exec(); moveToThread(this); void work() for(int i = 0; i < 11; i++) emit signalProgressValue(i*10); sleep(1);
主界面類:
#ifndef WIDGET_H #define WIDGET_H #include <QWidget> #include <QProgressBar> #include "WorkThread.h" class Widget : public QWidget QProgressBar* m_progress;//進度條 WorkThread* m_thread;//工作線程 Widget(QWidget *parent = 0):QWidget(parent) m_progress = new QProgressBar(this); m_progress->move(10, 10); m_progress->setMinimum(0); m_progress->setMaximum(100); m_progress->setTextVisible(true); m_progress->resize(100, 30); m_thread = new WorkThread(); m_thread->start(); connect(m_thread, SIGNAL(finished()), m_thread, SLOT(deleteLater())); //連接工作線程的信號到界面的槽函數 connect(m_thread, SIGNAL(signalProgressValue(int)), this, SLOT(onProgress(int))); ~Widget() void onProgress(int value) m_progress->setValue(value); #endif // WIDGET_H
Main函數:
#include "Widget.h" #include <QApplication> QApplication a(argc, argv); Widget w; w.show(); return a.exec();
3、發送自定義事件方式
A、自定義事件用於描述界面更新細節
B、在主窗口類中重寫事件處理函數event
C、使用postEvent函數(異步方式)發送自定義事件類對象
子線程指定接收消息的對象為主窗口對象,在event事件處理函數更新界面狀態
事件對象在主線程中被處理,event函數在主線程中調用。
發送的事件對象必須在堆空間創建
子線程創建時必須附帶目標對象的地址信息
自定義事件類:
#ifndef PROGRESSEVENT_H #define PROGRESSEVENT_H #include <QEvent> class ProgressEvent : public QEvent int m_progress; const static Type TYPE = static_cast<Type>(QEvent::User + 0xFF); ProgressEvent(int progress = 0):QEvent(TYPE) m_progress = progress; int progress()const return m_progress; #endif // PROGRESSEVENT_H
自定義線程類:
#include <ProgressEvent.h> QApplication::postEvent(parent(), new ProgressEvent(i*10));
自定義界面類:
#ifndef WIDGETUI_H #define WIDGETUI_H #include "ProgressEvent.h" class WidgetUI : public QWidget WidgetUI(QWidget *parent = 0):QWidget(parent) m_thread->setParent(this); ~WidgetUI() m_thread->quit(); bool event(QEvent *event) bool ret = true; if(event->type() == ProgressEvent::TYPE) ProgressEvent* evt = dynamic_cast<ProgressEvent*>(event); if(evt != NULL) //設置進度條的進度為事件參數的值 m_progress->setValue(evt->progress()); else ret = QWidget::event(event); return ret; #endif // WIDGETUI_H #include "WidgetUI.h" WidgetUI w;
到此這篇關於深入理解QT多線程編程 的文章就介紹到這瞭,更多相關QT多線程編程 內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!