一、線程基礎
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[ ];
- for( int i = ; i < ; i++)
- {
- p[i] = i * i;
- }
- sleep( );
- 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 = ;
- switch (flag) {
- case :
- case :
- mutex.unlock();
- return moreComplexFunction(flag);
- case :
- {
- int status = anotherFunction();
- if (status < ) {
- mutex.unlock();
- return ;
- }
- retVal = status + flag;
- }
- break;
- default:
- if (flag > ) {
- mutex.unlock();
- return ;
- }
- break;
- }
- 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);
互斥鎖加鎖: 從聲明處開始(在構造函數中加鎖)
互斥鎖解鎖: 出了作用域自動解鎖(在析構函數中解鎖)
- QMutex mutex;
- int complexFunction(int flag)
- {
- QMutexLocker locker(&mutex);
- int retVal = ;
- switch (flag) {
- case :
- case :
- return moreComplexFunction(flag);
- case :
- {
- int status = anotherFunction();
- if (status < )
- return ;
- retVal = status + flag;
- }
- break;
- default:
- if (flag > )
- return ;
- break;
- }
- return retVal;
- }
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();
- lock .unlock();
- }
5、QReadLocker和QWriteLocker
在較複雜的函數和異常進行中對QReadWriterLock類lock對象進行lockForRead()/lockForWrite()和unlock()操作将會很複雜,進入點要lockForRead()/lockForWrite(),在所有跳出點都要unlock(),很容易出現在某些跳出點未調用unlock(),是以Qt引進了QReadLocker和QWriteLocker類來簡化解鎖操作。在函數需要的地方建立QReadLocker或QWriteLocker對象,并把lock指針傳給QReadLocker或QWriteLocker對象,此時lock已經加鎖,等到退出函數後,QReadLocker或QWriteLocker對象局部變量會自己銷毀,此時lock解鎖。
- QReadWriteLock lock;
- QByteArray readData()
- {
- lock.lockForRead();
- ...
- lock.unlock();
- return data;
- }
使用QReadLocker:
- QReadWriteLock lock;
- QByteArray readData()
- {
- QReadLocker locker(&lock);
- ...
- return data;
- }
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 <QThread>
#include <cstdlib>
#include <cstdio>
const int DataSize = 100000;
const int BufferSize = 8192;
char buffer[BufferSize];
QSemaphore production(BufferSize);
QSemaphore consumption;
class Producor:public QThread
{
public:
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
{
public:
void run();
};
void Consumer::run()
{
for(int i = 0; i < DataSize; i++)
{
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 <QtCore/QCoreApplication>
#include <QSemaphore>
#include <QThread>
#include <cstdlib>
#include <cstdio>
#include <QWaitCondition>
#include <QMutex>
#include <QTime>
const int DataSize = 32;
const int BufferSize = 16;
char buffer[BufferSize];
QWaitCondition bufferNotEmpty;
QWaitCondition bufferNotFull;
QMutex mutex;
int used = 0;
class Producor:public QThread
{
public:
void run();
};
void Producor::run()
{
qsrand(QTime(0,0,0).secsTo(QTime::currentTime()));
for(int i = 0; i < DataSize; i++)
{
mutex.lock();
if(used == BufferSize)
bufferNotFull.wait(&mutex);
mutex.unlock();
buffer[i%BufferSize] = used;
mutex.lock();
used++;
bufferNotEmpty.wakeAll();
mutex.unlock();
}
}
class Consumer:public QThread
{
public:
void run();
};
void Consumer::run()
{
for(int i = 0; i < DataSize; i++)
{
mutex.lock();
if(used == 0)
bufferNotEmpty.wait(&mutex);
mutex.unlock();
fprintf(stderr, "%d\n", buffer[i%BufferSize]);
mutex.lock();
used--;
bufferNotFull.wakeAll();
mutex.unlock();
}
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();
}
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)和網絡類都不是線程安全的。
class Counter
{
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
#include <QThread>
class SyncThread : public QThread
{
Q_OBJECT
protected:
void run()
{
}
public:
explicit SyncThread(QObject* parent = 0):QThread(parent)
{
}
~SyncThread()
{
wait();
}
};
#endif // SYNCTHREAD_H
3、異步線程類設計
線程生命期結束時通知線程對象銷毀。
隻能在堆空間建立線程對象,線程對象不能被外界主動銷毀。
在run函數中最後調用deleteLater()函數。
線程函數主動申請銷毀線程對象。
使用場合:
線程生命期不可控,需要長時間運作于背景的線程。
#ifndef ASYNCTHREAD_H
#define ASYNCTHREAD_H
#include <QThread>
class AsyncThread : public QThread
{
Q_OBJECT
protected:
void run()
{
deleteLater();
}
explicit AsyncThread(QObject* parent = 0):QThread(parent)
{
}
~AsyncThread()
{
}
public:
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>
#include <QThread>
#include <QDebug>
class Worker : public QObject
{
Q_OBJECT
QThread m_thread;
protected slots:
void tmain()
{
qDebug() << "void tmain()";
}
public:
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線程(主線程)。
子線程更新界面狀态的本質是子線程發送信号通知主線程界面更新請求,主線程根據具體信号以及信号參數對界面元件進行修改。
使用信号槽在子線程中更新主界面中進度條的進度顯示資訊。
工作線程類:
#ifndef WORKTHREAD_H
#define WORKTHREAD_H
#include <QThread>
class WorkThread : public QThread
{
Q_OBJECT
signals:
void signalProgressValue(int value);
protected:
void run()
{
work();
exec();
}
public:
WorkThread()
{
m_stop = false;
moveToThread(this);
}
void work()
{
for(int i = 0; i < 11; i++)
{
emit signalProgressValue(i*10);
sleep(1);
}
}
};
#endif // WORKTHREAD_H
主界面類:
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QProgressBar>
#include "WorkThread.h"
class Widget : public QWidget
{
Q_OBJECT
QProgressBar* m_progress;//進度條
WorkThread* m_thread;//工作線程
public:
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()
{
}
protected slots:
void onProgress(int value)
{
m_progress->setValue(value);
}
};
#endif // WIDGET_H
Main函數:
#include "Widget.h"
#include <QApplication>
int main(int argc, char *argv[])
{
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;
public:
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
自定義線程類:
#ifndef WORKTHREAD_H
#define WORKTHREAD_H
#include <QThread>
#include <QApplication>
#include <ProgressEvent.h>
class WorkThread : public QThread
{
Q_OBJECT
protected:
volatile bool m_stop;
void run()
{
work();
exec();
}
public:
WorkThread()
{
m_stop = false;
}
void stop()
{
m_stop = true;
}
void work()
{
for(int i = 0; i < 11; i++)
{
QApplication::postEvent(parent(), new ProgressEvent(i*10));
sleep(1);
}
}
};
#endif // WORKTHREAD_H
自定義界面類:
#ifndef WIDGETUI_H
#define WIDGETUI_H
#include <QWidget>
#include <QProgressBar>
#include "WorkThread.h"
#include "ProgressEvent.h"
class WidgetUI : public QWidget
{
Q_OBJECT
QProgressBar* m_progress;//進度條
WorkThread* m_thread;//工作線程
public:
WidgetUI(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->setParent(this);
m_thread->start();
}
~WidgetUI()
{
m_thread->quit();
}
protected:
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
Main函數:
#include "WidgetUI.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
WidgetUI w;
w.show();
return a.exec();
}
一、線程基礎
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[ ];
- for( int i = ; i < ; i++)
- {
- p[i] = i * i;
- }
- sleep( );
- 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 = ;
- switch (flag) {
- case :
- case :
- mutex.unlock();
- return moreComplexFunction(flag);
- case :
- {
- int status = anotherFunction();
- if (status < ) {
- mutex.unlock();
- return ;
- }
- retVal = status + flag;
- }
- break;
- default:
- if (flag > ) {
- mutex.unlock();
- return ;
- }
- break;
- }
- 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);
互斥鎖加鎖: 從聲明處開始(在構造函數中加鎖)
互斥鎖解鎖: 出了作用域自動解鎖(在析構函數中解鎖)
- QMutex mutex;
- int complexFunction(int flag)
- {
- QMutexLocker locker(&mutex);
- int retVal = ;
- switch (flag) {
- case :
- case :
- return moreComplexFunction(flag);
- case :
- {
- int status = anotherFunction();
- if (status < )
- return ;
- retVal = status + flag;
- }
- break;
- default:
- if (flag > )
- return ;
- break;
- }
- return retVal;
- }
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();
- lock .unlock();
- }
5、QReadLocker和QWriteLocker
在較複雜的函數和異常進行中對QReadWriterLock類lock對象進行lockForRead()/lockForWrite()和unlock()操作将會很複雜,進入點要lockForRead()/lockForWrite(),在所有跳出點都要unlock(),很容易出現在某些跳出點未調用unlock(),是以Qt引進了QReadLocker和QWriteLocker類來簡化解鎖操作。在函數需要的地方建立QReadLocker或QWriteLocker對象,并把lock指針傳給QReadLocker或QWriteLocker對象,此時lock已經加鎖,等到退出函數後,QReadLocker或QWriteLocker對象局部變量會自己銷毀,此時lock解鎖。
- QReadWriteLock lock;
- QByteArray readData()
- {
- lock.lockForRead();
- ...
- lock.unlock();
- return data;
- }
使用QReadLocker:
- QReadWriteLock lock;
- QByteArray readData()
- {
- QReadLocker locker(&lock);
- ...
- return data;
- }
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 <QThread>
#include <cstdlib>
#include <cstdio>
const int DataSize = 100000;
const int BufferSize = 8192;
char buffer[BufferSize];
QSemaphore production(BufferSize);
QSemaphore consumption;
class Producor:public QThread
{
public:
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
{
public:
void run();
};
void Consumer::run()
{
for(int i = 0; i < DataSize; i++)
{
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 <QtCore/QCoreApplication>
#include <QSemaphore>
#include <QThread>
#include <cstdlib>
#include <cstdio>
#include <QWaitCondition>
#include <QMutex>
#include <QTime>
const int DataSize = 32;
const int BufferSize = 16;
char buffer[BufferSize];
QWaitCondition bufferNotEmpty;
QWaitCondition bufferNotFull;
QMutex mutex;
int used = 0;
class Producor:public QThread
{
public:
void run();
};
void Producor::run()
{
qsrand(QTime(0,0,0).secsTo(QTime::currentTime()));
for(int i = 0; i < DataSize; i++)
{
mutex.lock();
if(used == BufferSize)
bufferNotFull.wait(&mutex);
mutex.unlock();
buffer[i%BufferSize] = used;
mutex.lock();
used++;
bufferNotEmpty.wakeAll();
mutex.unlock();
}
}
class Consumer:public QThread
{
public:
void run();
};
void Consumer::run()
{
for(int i = 0; i < DataSize; i++)
{
mutex.lock();
if(used == 0)
bufferNotEmpty.wait(&mutex);
mutex.unlock();
fprintf(stderr, "%d\n", buffer[i%BufferSize]);
mutex.lock();
used--;
bufferNotFull.wakeAll();
mutex.unlock();
}
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();
}
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)和網絡類都不是線程安全的。
class Counter
{
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
#include <QThread>
class SyncThread : public QThread
{
Q_OBJECT
protected:
void run()
{
}
public:
explicit SyncThread(QObject* parent = 0):QThread(parent)
{
}
~SyncThread()
{
wait();
}
};
#endif // SYNCTHREAD_H
3、異步線程類設計
線程生命期結束時通知線程對象銷毀。
隻能在堆空間建立線程對象,線程對象不能被外界主動銷毀。
在run函數中最後調用deleteLater()函數。
線程函數主動申請銷毀線程對象。
使用場合:
線程生命期不可控,需要長時間運作于背景的線程。
#ifndef ASYNCTHREAD_H
#define ASYNCTHREAD_H
#include <QThread>
class AsyncThread : public QThread
{
Q_OBJECT
protected:
void run()
{
deleteLater();
}
explicit AsyncThread(QObject* parent = 0):QThread(parent)
{
}
~AsyncThread()
{
}
public:
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>
#include <QThread>
#include <QDebug>
class Worker : public QObject
{
Q_OBJECT
QThread m_thread;
protected slots:
void tmain()
{
qDebug() << "void tmain()";
}
public:
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線程(主線程)。
子線程更新界面狀态的本質是子線程發送信号通知主線程界面更新請求,主線程根據具體信号以及信号參數對界面元件進行修改。
使用信号槽在子線程中更新主界面中進度條的進度顯示資訊。
工作線程類:
#ifndef WORKTHREAD_H
#define WORKTHREAD_H
#include <QThread>
class WorkThread : public QThread
{
Q_OBJECT
signals:
void signalProgressValue(int value);
protected:
void run()
{
work();
exec();
}
public:
WorkThread()
{
m_stop = false;
moveToThread(this);
}
void work()
{
for(int i = 0; i < 11; i++)
{
emit signalProgressValue(i*10);
sleep(1);
}
}
};
#endif // WORKTHREAD_H
主界面類:
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QProgressBar>
#include "WorkThread.h"
class Widget : public QWidget
{
Q_OBJECT
QProgressBar* m_progress;//進度條
WorkThread* m_thread;//工作線程
public:
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()
{
}
protected slots:
void onProgress(int value)
{
m_progress->setValue(value);
}
};
#endif // WIDGET_H
Main函數:
#include "Widget.h"
#include <QApplication>
int main(int argc, char *argv[])
{
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;
public:
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
自定義線程類:
#ifndef WORKTHREAD_H
#define WORKTHREAD_H
#include <QThread>
#include <QApplication>
#include <ProgressEvent.h>
class WorkThread : public QThread
{
Q_OBJECT
protected:
volatile bool m_stop;
void run()
{
work();
exec();
}
public:
WorkThread()
{
m_stop = false;
}
void stop()
{
m_stop = true;
}
void work()
{
for(int i = 0; i < 11; i++)
{
QApplication::postEvent(parent(), new ProgressEvent(i*10));
sleep(1);
}
}
};
#endif // WORKTHREAD_H
自定義界面類:
#ifndef WIDGETUI_H
#define WIDGETUI_H
#include <QWidget>
#include <QProgressBar>
#include "WorkThread.h"
#include "ProgressEvent.h"
class WidgetUI : public QWidget
{
Q_OBJECT
QProgressBar* m_progress;//進度條
WorkThread* m_thread;//工作線程
public:
WidgetUI(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->setParent(this);
m_thread->start();
}
~WidgetUI()
{
m_thread->quit();
}
protected:
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
Main函數:
#include "WidgetUI.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
WidgetUI w;
w.show();
return a.exec();
}