QT經過三種形式提供了對線程的支持。它們各自是,html
1、平臺無關的線程類
2、線程安全的事件投遞
3、跨線程的信號-槽鏈接。c++
這使得開發輕巧的多線程Qt程序更爲easy,並能充分利用多處理器機器的優點。編程
多線程編程也是一個實用的模式。它用於解決運行較長時間的操做而不至於用戶界面失去響應。安全
在Qt的早期版本號中。在構建庫時有不選擇線程支持的選項,從4.0開始,線程老是有效的。markdown
Qt 包括如下一些線程相關的類:多線程
這篇文章在這個問題上有着更加人性化的展現。併發
爲建立一個線程。子類化QThread而且重寫它的run()函數,好比:app
class MyThread : public QThread
{
Q_OBJECT
protected:
void run();
};
void MyThread::run()
{
...
}
以後,建立這個線程對象的實例。調用QThread::start()。因而。在run()裏出現的代碼將會在另外線程中被運行。函數
注意:QCoreApplication::exec()必須老是在主線程(運行main()的那個線程)中被調用,不能從一個QThread中調用。在GUI程序中。主線程也被稱爲GUI線程,因爲它是惟一一個贊成運行GUI相關操做的線程。post
另外,你必須在建立一個QThread以前建立QApplication(or QCoreApplication)對象。
這裏的同步模式可以去看看這兩篇文章,提供的是後一篇的鏈接。
QMutex, QReadWriteLock, QSemaphore, QWaitCondition 提供了線程同步的手段。使用線程的主要想法是但願它們可以儘量併發運行,而一些關鍵點上線程之間需要中止或等待。
好比,假如兩個線程試圖同一時候訪問同一個全局變量。結果可能不如所願。
QMutex 提供相互排斥的鎖,或相互排斥量。在一個時刻至多一個線程擁有mutex,假如一個線程試圖訪問已經被鎖定的mutex,那麼它將休眠,直到擁有mutex的線程對此mutex解鎖。
Mutexes常用來保護共享數據訪問。
QReadWriterLock 與QMutex相似,除了它對 「read」,」write」訪問進行差異對待。它使得多個讀者可以共時訪問數據。使用QReadWriteLock而不是QMutex。可以使得多線程程序更具備併發性。
QReadWriteLock lock;
void ReaderThread::run()
{
// ...
lock.lockForRead();
read_file();
lock.unlock();
//...
}
void WriterThread::run()
{
// ...
lock.lockForWrite();
write_file();
lock.unlock();
// ...
}
QSemaphore 是QMutex的通常化,它可以保護必定數量的相同資源。與此相對。一個mutex僅僅保護一個資源。
如下樣例中,使用QSemaphore來控制對環狀緩衝的訪問,此緩衝區被生產者線程和消費者線程共享。生產者不斷向緩衝寫入數據直到緩衝末端,再從頭開始。消費者從緩衝不斷讀取數據。信號量比相互排斥量有更好的併發性,假如咱們用相互排斥量來控制對緩衝的訪問,那麼生產者,消費者不能同一時候訪問緩衝。
然而。咱們知道在同一時刻。不一樣線程訪問緩衝的不一樣部分並無什麼危害。
const int DataSize = 100000;
const int BufferSize = 8192;
char buffer[BufferSize];
QSemaphore freeBytes(BufferSize);
QSemaphore usedBytes;
class Producer : public QThread
{
public:
void run();
};
void Producer::run()
{
qsrand(QTime(0,0,0).secsTo(QTime::currentTime()));
for (int i = 0; i < DataSize; ++i) {
freeBytes.acquire();
buffer[i % BufferSize] = "ACGT"[(int)qrand() % 4];
usedBytes.release();
}
}
class Consumer : public QThread
{
public:
void run();
};
void Consumer::run()
{
for (int i = 0; i < DataSize; ++i) {
usedBytes.acquire();
fprintf(stderr, "%c", buffer[i % BufferSize]);
freeBytes.release();
}
fprintf(stderr, "\n");
}
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
Producer producer;
Consumer consumer;
producer.start();
consumer.start();
producer.wait();
consumer.wait();
return 0;
}
QWaitCondition 贊成線程在某些狀況發生時喚醒另外的線程。一個或多個線程可以堵塞等待一QWaitCondition ,用wakeOne()或wakeAll()設置一個條件。
wakeOne()隨機喚醒一個,wakeAll()喚醒所有。
如下的樣例中。生產者首先必須檢查緩衝是否已滿(numUsedBytes==BufferSize),假設是,線程停下來等待bufferNotFull條件。
假設不是,在緩衝中生產數據。添加numUsedBytes,激活條件 bufferNotEmpty。使用mutex來保護對numUsedBytes的訪問。
另外,QWaitCondition::wait()接收一個mutex做爲參數,這個mutex應該被調用線程初始化爲鎖定狀態。在線程進入休眠狀態以前,mutex會被解鎖。
而當線程被喚醒時。mutex會處於鎖定狀態,而且,從鎖定狀態到等待狀態的轉換是原子操做,這阻止了競爭條件的產生。當程序開始運行時,僅僅有生產者可以工做。消費者被堵塞等待bufferNotEmpty條件,一旦生產者在緩衝中放入一個字節。bufferNotEmpty條件被激發。消費者線程因而被喚醒。
const int DataSize = 100000;
const int BufferSize = 8192;
char buffer[BufferSize];
QWaitCondition bufferNotEmpty;
QWaitCondition bufferNotFull;
QMutex mutex;
int numUsedBytes = 0;
class Producer : public QThread
{
public:
void run();
};
void Producer::run()
{
qsrand(QTime(0,0,0).secsTo(QTime::currentTime()));
for (int i = 0; i < DataSize; ++i) {
mutex.lock();
if (numUsedBytes == BufferSize)
bufferNotFull.wait(&mutex);
mutex.unlock();
buffer[i % BufferSize] = "ACGT"[(int)qrand() % 4];
mutex.lock();
++numUsedBytes;
bufferNotEmpty.wakeAll();
mutex.unlock();
}
}
class Consumer : public QThread
{
public:
void run();
};
void Consumer::run()
{
for (int i = 0; i < DataSize; ++i) {
mutex.lock();
if (numUsedBytes == 0)
bufferNotEmpty.wait(&mutex);
mutex.unlock();
fprintf(stderr, "%c", buffer[i % BufferSize]);
mutex.lock();
--numUsedBytes;
bufferNotFull.wakeAll();
mutex.unlock();
}
fprintf(stderr, "\n");
}
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
Producer producer;
Consumer consumer;
producer.start();
consumer.start();
producer.wait();
consumer.wait();
return 0;
}
在Qt文檔中。術語「可重入」與「線程安全」被用來講明一個函數怎樣用於多線程程序。假如一個類的不論什麼函數在此類的多個不一樣的實例上,可以被多個線程同一時候調用。那麼這個類被稱爲是「可重入」的。假如不一樣的線程做用在同一個實例上仍可以正常工做。那麼稱之爲「線程安全」的。
大多數c++類天生就是可重入的。因爲它們典型地僅僅引用成員數據。不論什麼線程可以在類的一個實例上調用這樣的成員函數,僅僅要沒有別的線程在同一個實例上調用這個成員函數。舉例來說,如下的Counter 類是可重入的:
class Counter
{
public:
Counter() {n=0;}
void increment() {++n;}
void decrement() {--n;}
int value() const {return n;}
private:
int n;
};
這個類不是線程安全的,因爲假如多個線程都試圖改動數據成員 n,結果沒有定義。這是因爲c++中的++和–操做符不是原子操做。
實際上,它們會被擴展爲三個機器指令:
1,把變量值裝入寄存器
2。添加或下降寄存器中的值
3。把寄存器中的值寫回內存
假如線程A與B同一時候裝載變量的舊值,在寄存器中增值,回寫。他們寫操做重疊了。致使變量值僅添加了一次。
很是明顯。訪問應該串行化:A運行123步驟時不該被打斷。使這個類成爲線程安全的最簡單方法是使用QMutex來保護數據成員:
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;
};
QMutexLocker類在構造函數中本身主動對mutex進行加鎖,在析構函數中進行解鎖。
隨便一提的是。mutex使用了mutable關鍵字來修飾,因爲咱們在value()函數中對mutex進行加鎖與解鎖操做,而value()是一個const函數。
大多數Qt類是可重入。非線程安全的。有一些類與函數是線程安全的,它們主要是線程相關的類,如QMutex,QCoreApplication::postEvent()。
QThread 繼承自QObject,它發射信號以指示線程運行開始與結束,而且也提供了不少slots。更有趣的是,QObjects可以用於多線程,這是因爲每個線程被贊成有它本身的事件循環。
QObject 可重入性
QObject是可重入的。
它的大多數非GUI子類,像QTimer,QTcpSocket,QUdpSocket,QHttp,QFtp,QProcess也是可重入的,在多個線程中同一時候使用這些類是可能的。需要注意的是。這些類被設計成在一個單線程中建立與使用,所以。在一個線程中建立一個對象,而在另外的線程中調用它的函數,這樣的行爲不能保證工做良好。
有三種約束需要注意:
1。QObject的孩子老是應該在它父親被建立的那個線程中建立。
這意味着,你毫不應該傳遞QThread對象做爲還有一個對象的父親(因爲QThread對象自己會在還有一個線程中被建立)
2,事件驅動對象僅僅在單線程中使用。明白地說,這個規則適用於」定時器機制「與」網格模塊「,舉例來說,你不該該在一個線程中開始一個定時器或是鏈接一個套接字。當這個線程不是這些對象所在的線程。
3,你必須保證在線程中建立的所有對象在你刪除QThread前被刪除。
這很是easy作到:你可以run()函數運行的棧上建立對象。
雖然QObject是可重入的,但GUI類,特別是QWidget與它的所有子類都是不可重入的。它們僅用於主線程。
正如前面提到過的,QCoreApplication::exec()也必須從那個線程中被調用。
實踐上,不會在別的線程中使用GUI類,它們工做在主線程上,把一些耗時的操做放入獨立的工做線程中。當工做線程運行完畢,把結果在主線程所擁有的屏幕上顯示。
每個線程可以有它的事件循環,初始線程開始它的事件循環需使用QCoreApplication::exec(),別的線程開始它的事件循環需要用QThread::exec().像QCoreApplication同樣,QThreadr提供了exit(int)函數。一個quit() slot。
線程中的事件循環,使得線程可以使用那些需要事件循環的非GUI 類(如。QTimer,QTcpSocket,QProcess)。
也可以把不論什麼線程的signals鏈接到特定線程的slots,也就是說信號-槽機制是可以跨線程使用的。對於在QApplication以前建立的對象。QObject::thread()返回0,這意味着主線程僅爲這些對象處理投遞事件。不會爲沒有所屬線程的對象處理另外的事件。
可以用QObject::moveToThread()來改變它和它孩子們的線程親緣關係。假如對象有父親。它不能移動這樣的關係。在還有一個線程(而不是建立它的那個線程)中delete QObject對象是不安全的。除非你可以保證在同一時刻對象不在處理事件。
可以用QObject::deleteLater(),它會投遞一個DeferredDelete事件,這會被對象線程的事件循環終於選取到。
假如沒有事件循環運行,事件不會分發給對象。舉例來講,假如你在一個線程中建立了一個QTimer對象,但從沒有調用過exec(),那麼QTimer就不會發射它的timeout()信號.對deleteLater()也不會工做。
(這相同適用於主線程)。你可以手工使用線程安全的函數QCoreApplication::postEvent()。在不論何時,給不論什麼線程中的不論什麼對象投遞一個事件,事件會在那個建立了對象的線程中經過事件循環派發。
事件過濾器在所有線程中也被支持。只是它限定被監視對象與監視對象生存在同一線程中。相似地,QCoreApplication::sendEvent(不是postEvent()),僅用於在調用此函數的線程中向目標對象投遞事件。
QObject和所有它的子類是非線程安全的。這包括整個的事件投遞系統。需要牢記的是,當你正從別的線程中訪問對象時,事件循環可以向你的QObject子類投遞事件。假如你調用一個不生存在當前線程中的QObject子類的函數時,你必須用mutex來保護QObject子類的內部數據,不然會遭遇災難或非預期結果。
像其餘的對象同樣,QThread對象生存在建立它的那個線程中—不是當QThread::run()被調用時建立的那個線程。
通常來說,在你的QThread子類中提供slots是不安全的,除非你用mutex保護了你的成員變量。
還有一方面,你可以安全的從QThread::run()的實現中發射信號,因爲信號發射是線程安全的。
Qt支持三種類型的信號-槽鏈接:
1。直接鏈接,當signal發射時,slot立刻調用。
此slot在發射signal的那個線程中被運行(不必定是接收對象生存的那個線程)
2,隊列鏈接。當控制權回到對象屬於的那個線程的事件循環時。slot被調用。此slot在接收對象生存的那個線程中被運行
3,本身主動鏈接(缺省)。假如信號發射與接收者在同一個線程中,其行爲如直接鏈接。不然,其行爲如隊列鏈接。
鏈接類型可能經過以向connect()傳遞參數來指定。注意的是,當發送者與接收者生存在不一樣的線程中,而事件循環正運行於接收者的線程中,使用直接鏈接是不安全的。相同的道理。調用生存在不一樣的線程中的對象的函數也是否是安全的。QObject::connect()自己是線程安全的。
Qt爲它的不少值類型使用了所謂的隱含共享(implicit sharing)來優化性能。原理比較簡單,共享類包括一個指向共享數據塊的指針,這個數據塊中包括了真正原數據與一個引用計數。把深拷貝轉化爲一個淺拷貝,從而提升了性能。這樣的機制在幕後發生做用,程序猿不需要關心它。假設深刻點看,假如對象需要對數據進行改動,而引用計數大於1。那麼它應該先detach()。以使得它改動不會對別的共享者產生影響。既然改動後的數據與原來的那份數據不一樣了,所以不可能再共享了,因而它先運行深拷貝,把數據取回來。再在這份數據上進行改動。
好比:
void QPen::setStyle(Qt::PenStyle style)
{
detach(); // detach from common data
d->style = style; // set the style member
}
void QPen::detach()
{
if (d->ref != 1) {
... // perform a deep copy
}
}
通常以爲,隱含共享與多線程不太和諧,因爲有引用計數的存在。對引用計數進行保護的方法之中的一個是使用mutex,但它很是慢。Qt早期版本號沒有提供一個愜意的解決方式。從4.0開始。隱含共享類可以安全地跨線程拷貝。如同別的值類型同樣。
它們是全然可重入的。
隱含共享真的是」implicit」。
它使用匯編語言實現了原子性引用計數操做,這比用mutex快多了。 假如你在多個線程中同進訪問相同對象,你也需要用mutex來串行化訪問順序,就如同其餘可重入對象那樣。總的來說,隱含共享真的給」隱含「掉了,在多線程程序中。你可以把它們當作是通常的,非共享的,可重入的類型,這樣的作法是安全的。