Qt學習 之 多線程程序設計

QT經過三種形式提供了對線程的支持。它們各自是,html

1、平臺無關的線程類
2、線程安全的事件投遞
3、跨線程的信號-槽鏈接。c++

這使得開發輕巧的多線程Qt程序更爲easy,並能充分利用多處理器機器的優點。編程

多線程編程也是一個實用的模式。它用於解決運行較長時間的操做而不至於用戶界面失去響應。安全

在Qt的早期版本號中。在構建庫時有不選擇線程支持的選項,從4.0開始,線程老是有效的。markdown

線程類

Qt 包括如下一些線程相關的類:多線程

  • QThread 提供了開始一個新線程的方法
  • QThreadStorage 提供逐線程數據存儲
  • QMutex 提供相互排斥的鎖,或相互排斥量
  • QMutexLocker 是一個便利類,它可以本身主動對QMutex加鎖與解鎖
  • QReadWriterLock 提供了一個可以同一時候讀操做的鎖
  • QReadLocker與QWriteLocker 是便利類。它本身主動對QReadWriteLock加鎖與解鎖
  • QSemaphore 提供了一個整型信號量,是相互排斥量的泛化
  • QWaitCondition 提供了一種方法。使得線程可以在被另外線程喚醒以前一直休眠。

建立一個線程

這篇文章在這個問題上有着更加人性化的展現。併發

爲建立一個線程。子類化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()。

線程與QObjects

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子類投遞事件。假如你調用一個不生存在當前線程中的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來串行化訪問順序,就如同其餘可重入對象那樣。總的來說,隱含共享真的給」隱含「掉了,在多線程程序中。你可以把它們當作是通常的,非共享的,可重入的類型,這樣的作法是安全的。

相關文章
相關標籤/搜索