QT開發(三十四)——QT多線程編程

QT開發(三十四)——QT多線程編程

1、線程基礎

1GUI線程與工做線程

每一個程序啓動後擁有的第一個線程稱爲主線程,即GUI線程。QT中全部的組件類和幾個相關的類只能工做在GUI線程,不能工做在次線程,次線程即工做線程,主要負責處理GUI線程卸下的工做。數據庫

二、數據的同步訪問

每一個線程都有本身的棧,所以每一個線程都要本身的調用歷史和本地變量。線程共享相同的地址空間。編程

2、QT多線程簡介

    QT經過三種形式提供了對線程的支持,分別是平臺無關的線程類、線程安全的事件投遞、跨線程的信號-槽鏈接。安全

    QT中線程類包含以下:網絡

 QThread 提供了跨平臺的多線程解決方案多線程

 QThreadStorage 提供逐線程數據存儲
    QMutex 提供相互排斥的鎖,或互斥量
    QMutexLocker 是一個輔助類,自動對 QMutex 加鎖與解鎖
    QReadWriterLock 提供了一個能夠同時讀操做的鎖
    QReadLocker與QWriteLocker 自動對QReadWriteLock 加鎖與解鎖
    QSemaphore 提供了一個整型信號量,是互斥量的泛化
    QWaitCondition 提供了一種方法,使得線程能夠在被另外線程喚醒以前一直休眠。併發

3、QThread線程

1QThread線程基礎

    QThreadQt線程中有一個公共的抽象類,全部的線程類都是從QThread抽象類中派生的,須要實現QThread中的虛函數run(),經過start()函數來調用run函數框架

    void run()函數是線程體函數,用於定義線程的功能。less

    void start()函數是啓動函數,用於將線程入口地址設置爲run函數。異步

    void terminate()函數用於強制結束線程,不保證數據完整性和資源釋放。socket

    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、線程與事件循環

    QThreadrun()的默認實現調用了exec(),從而建立一個QEventLoop對象,由QEventLoop對象處理線程中事件隊列(每個線程都有一個屬於本身的事件隊列)中的事件。exec()在其內部不斷作着循環遍歷事件隊列的工做,調用QThreadquit()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()),僅用於在調用此函數的線程中向目標對象投遞事件。

4、線程的同步

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

ARecursive

    一個線程能夠對mutex屢次lock,直到相應次數的unlock調用後,mutex才真正被解鎖。

BNonRecursive

    默認模式,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;
         }
         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 = 0;
     switch (flag) {
     case 0:
     case 1:
         return moreComplexFunction(flag);
     case 2:
         {
             int status = anotherFunction();
             if (status < 0)
                 return -2;
             retVal = status + flag;
         }
         break;
     default:
         if (flag > 10)
             return -1;
         break;
     }
     return retVal;
 }


4QReadWriteLock

    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();
 }


5QReadLockerQWriteLocker

    在較複雜的函數和異常處理中對QReadWriterLocklock對象進行lockForRead()/lockForWrite()和unlock()操做將會很複雜,進入點要lockForRead()/lockForWrite(),在全部跳出點都要unlock(),很容易出如今某些跳出點未調用unlock(),因此Qt引進了QReadLocker和QWriteLocker類來簡化解鎖操做。在函數須要的地方創建QReadLockerQWriteLocker對象,並把lock指針傳給QReadLockerQWriteLocker對象,此時lock已經加鎖,等到退出函數後,QReadLockerQWriteLocker對象局部變量會本身銷燬,此時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來保護。

若是隻使用信號槽,而且線程間沒有共享變量,那麼,多線程程序能夠徹底沒有低級原語。

5、可重入與線程安全

可重入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函數。

6、線程與信號槽

1、線程的依附性

    線程的依附性是對象與線程的關係。默認狀況下,對象依附於自身被建立的線程。

    對象的依附性與槽函數執行的關係,默認狀況下,槽函數在其所依附的線程中被調用執行。

    修改對象的依附性的方法:QObject::moveToThread函數用於改變對象的線程依附性,使得對象的槽函數在依附的線程中被調用執行。

2QObject線程

QThread類具備發送信號和定義槽函數的能力。QThread主要信號以下:

void started();線程開始運行時發送信號

void finished();線程完成運行時發送信號

void terminated();線程被異常終止時發送信號

    QThread繼承自QObject,發射信號以指示線程執行開始與結束,並提供了許多槽函數。QObjects能夠用於多線程,發射信號以在其它線程中調用槽函數,而且向「存活」於其它線程中的對象發送事件
QObject的可重入性

    QObject是可重入的,QObject的大多數非GUI子類 QTimerQTcpSocketQUdpSocketQHttpQFtpQProcess也是可重入的,在多個線程中同時使用這些類是可能的。可重入的類被設計成在一個單線程中建立與使用,在一個線程中建立一個對象而在另外一個線程中調用該對象的函數,不保證能行得通。有三種約束須要注意:

    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組件狀態。

7、線程的設計

一、線程的生命週期

若是線程的正處於執行過程當中時,線程對象被銷燬時,程序將會出錯。

工程實踐中線程對象的生命期必須大於線程的生命期。

二、同步線程類設計

線程對象主動等待線程生命期結束後才銷燬,線程對象銷燬時確保線程執行結束,支持在棧或堆上建立線程對象。

在線程類的析構函數中先調用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

8、線程的使用方式

1、子類化QThread

QThread的兩種使用方法:

1)不使用事件循環

 A、子類化 QThread

    B、重寫run函數,run函數內有一個 while 或 for 的死循環

    C、設置一個標記爲來控制死循環的退出。

    適用於後臺執行長時間的耗時操做,如文件複製、網絡數據讀取。

2)使用事件循環。

    A、子類化 QThread

    B、重寫run 使其調用 QThread::exec() ,開啓線程的事件循環

C、爲子類定義信號和槽,因爲槽函數並不會在新開的 Thread 運行,在構造函數中調用 moveToThread(this)

適用於事務性操做,如文件讀寫、數據庫讀寫。

2Worker-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

9、多線程與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();

}

相關文章
相關標籤/搜索