QT中的線程與事件循環理解(2)

 

1. Qt多線程與Qobject的關係安全

  每個 Qt 應用程序至少有一個事件循環,就是調用了QCoreApplication::exec()的那個事件循環。不過,QThread也能夠開啓事件循環。只不過這是一個受限於線程內部的事件循環。所以咱們將處於調用main()函數的那個線程,而且由QCoreApplication::exec()建立開啓的那個事件循環成爲主事件循環,或者直接叫主循環。注意,QCoreApplication::exec()只能在調用main()函數的線程調用。主循環所在的線程就是主線程,也被成爲 GUI 線程,由於全部有關 GUI 的操做都必須在這個線程進行。QThread局部事件循環則能夠經過在QThread::run()中調用QThread::exec()開啓:多線程

class Thread : public QThread
{
protected:
    void run() {
        
    }
};

  注意:Qt 4.4 版本之後,QThread::run()再也不是純虛函數,它會調用QThread::exec()函數。與QCoreApplication同樣,QThread也有QThread::quit()QThread::exit()函數來終止事件循環併發

run 函數是作什麼用的?Manual中說的清楚:   app

run 對於線程的做用至關於main函數對於應用程序。它是線程的入口,run的開始和結束意味着線程的開始和結束。ide

The run() implementation is for a thread what the main() entry point is for the application. All code executed in a call stack that starts in the run() function is executed by the new thread, and the thread finishes when the function returns.函數

  線程的事件循環用於爲線程中的全部QObjects對象分發事件;默認狀況下,這些對象包括線程中建立的全部對象或者是在別處建立完成後被移動到該線程的對象(咱們會在後面詳細介紹「移動」這個問題)。咱們說,一個QObject的所依附的線程(thread affinity)是指它所在的那個線程。它一樣適用於在QThread的構造函數中構建的對象:post

class MyThread : public QThread
{
public:
    MyThread()
    {
        otherObj = new QObject;
    }    
 
private:
    QObject obj;
    QObject *otherObj;
    QScopedPointer yetAnotherObj;
};

  上面線程對象中的子成員:obj, 以及otherObj所指向的對象,以及yetAnotherObj,都在使建立Mytherad的線程,即主線程,而不是子線程。ui

  在咱們建立了MyThread對象以後,objotherObjyetAnotherObj的線程依附性是怎樣的?是否是就是MyThread所表示的那個線程?要回答這個問題,咱們必須看看到底是哪一個線程建立了它們:實際上,是調用了MyThread構造函數的線程建立了它們。所以,這些對象不在MyThread所表示的線程,而是在建立了MyThread的那個線程中  this

(1)QObject::connectspa

    涉及信號槽,咱們就躲不過 connect 函數,只是這個函數你們太熟悉。我很差意思再用一堆廢話來描述它,但不說又不行,那麼折中一下,只看它的最後一個參數吧(爲了簡單起見,只看它最經常使用的3個值):

  經過指定connect的鏈接方式,若是指定直接鏈接(Direct Connection),則該槽函數將再信號發出的線程中直接執行,而不用斷定當前信號發出的線程與槽函數所在線程的狀態;若是指定隊列鏈接(Queued Connection),則該槽函數在接受者所依附的線程的線程循環中被指定調用;若是爲自動鏈接(Auto Connection)須要斷定發射信號的線程和接受者所依附的線程是否相同,進行細分指定。

 

  • 自動鏈接(Auto Connection)
    • 這是默認設置
    • 若是發送者的信號(不是發送者對象)在接收者所依附的線程內發射,則等同於直接鏈接
    • 若是發射信號的線程接受者所依附的線程不一樣,則等同於隊列鏈接
    • 也就是這說,只存在下面兩種狀況
  • 直接鏈接(Direct Connection)
    • 當信號發射時,槽函數將直接被調用
    • 不管槽函數所屬對象在哪一個線程,槽函數都在發射信號的線程內執行
  • 隊列鏈接(Queued Connection)
    • 控制權回到接受者所依附線程的事件循環時,槽函數被調用
    • 槽函數在接收者所依附線程執行

(2)qT線程管理的原則:

  • QThread 是用來管理線程的,它所依附的線程它管理的線程並非同一個東西
  • QThread 所依附的線程,就是執行 QThread t 或 QThread * t=new QThread 所在的線程。也就是我們這兒的主線程
  • QThread 管理的線程,就是 run 啓動的線程。也就是次線程
  • 由於QThread的對象依附在主線程中,所以他的slot函數會在主線程中執行,而不是次線程。除非:
    • QThread 對象依附到次線程中(經過movetoThread)
    • slot 和信號是直接鏈接(經過connect鏈接方式來指定),且信號在次線程中發射

【1】主線程(信號)QThread(槽)

class Dummy:public QObject 
{ 
    Q_OBJECT 
public: 
    Dummy(){} 
public slots: 
    void emitsig() 
    { 
        emit sig(); 
    } 
signals: 
    void sig(); 
}; 
 
class Thread:public QThread 
{ 
    Q_OBJECT 
public: 
    Thread(QObject* parent=0):QThread(parent) 
    { 
        //moveToThread(this); 
    } 
public slots: 
    void slot_main() 
    { 
        qDebug()<<"from thread slot_main:" <<currentThreadId(); 
    } 
protected: 
    void run() 
    { 
        qDebug()<<"thread thread:"<<currentThreadId(); 
        exec(); 
    } 
}; 

#include "main.moc" 

int main(int argc, char *argv[]) 
{  
    QCoreApplication a(argc, argv); 
    qDebug()<<"main thread:"<<QThread::currentThreadId(); 
    Thread thread; //槽函數所在的對象依附於線程,
    Dummy dummy; 
    QObject::connect(&dummy, SIGNAL(sig()), &thread, SLOT(slot_main())); //採用默認的連接方式
    thread.start(); 
    dummy.emitsig();//信號在主線程中發射 return a.exec(); 
}

程序運行結果:

main thread: 0x1a40 
from thread slot_main: 0x1a40
thread thread: 0x1a48

  由於connect採用默認的連接方式,則須要斷定發射信號的線程和接受者所依附的線程是否相同,信號在主線程中發射 槽函數所在的對象依附於線程, 所以連接方式是直接鏈接,從而運行結果是:槽函數的線程Id和主線程ID是同樣的!

   由於slot和run處於不一樣線程,須要線程間的同步!

  你會發現 QThread 中 slot 和 run 函數共同操做的對象,都會用QMutex鎖住。由於此時run 是另外一個線程,即子線程。而slot則是在主線程執行。必須適應鎖來保證數據同步

  若是想讓槽函數slot在次線程運行(好比它執行耗時的操做,會讓主線程卡死)

  • 將 thread 依附的線程改成次線程不就好了,這也是代碼中註釋掉的 moveToThread(this)所作的

  去掉註釋,你會發現slot在次線程中運行結果:

main thread: 0x13c0 
thread thread: 0x1de0 
from thread slot_main: 0x1de0

  但這是 Bradley T. Hughes 強烈批判的用法。不推薦這樣使用

【2】run中信號與QThread中槽

  即,信號在子線程發射,而槽函數誰線程的槽函數,而線程對象在主線程中建立,若是連接採用自動連接,則條件判斷比爲隊列鏈接,且由主線程在主線程的事件循環中執行。以下所示:

class Dummy:public QObject 
{ 
    Q_OBJECT 
public: 
    Dummy(QObject* parent=0):QObject(parent){} 
public slots: 
    void emitsig() 
    { 
        emit sig(); 
    } 
signals: 
    void sig(); 
}; 
class Thread:public QThread 
{ 
    Q_OBJECT 
public: 
    Thread(QObject* parent=0):QThread(parent) 
    { 
        //moveToThread(this); 
    } 
public slots: 
    void slot_thread() 
    { 
        qDebug()<<"from thread slot_thread:" <<currentThreadId(); 
    } 
signals: 
    void sig(); 
protected: 
    void run() 
    { 
        qDebug()<<"thread thread:"<<currentThreadId(); 
        Dummy dummy; 
        connect(&dummy, SIGNAL(sig()), this, SLOT(slot_thread())); 
        dummy.emitsig(); 
        exec(); 
    } 
}; 
#include "main.moc" 
int main(int argc, char *argv[]) 
{ 
    QCoreApplication a(argc, argv); 
    qDebug()<<"main thread:"<<QThread::currentThreadId(); 
    Thread thread; 
    thread.start(); 
    return a.exec(); 
}
View Code

運行結果:槽函數在主線程中執行。

main thread: 0x15c0

thread thread: 0x1750

from thread slot_thread: 0x15c0

  若是指定爲直接鏈接方式,則槽函數將在次線程(信號發出的線程)執行,這樣,你須要處理slot和它的對象所在線程的同步。須要 QMutex 一類的東西

推薦的方法

  其實,這個方法太簡單,太好用了。定義一個普通的QObject派生類,而後將其對象move到QThread中。使用信號和槽時根本不用考慮多線程的存在。也不用使用QMutex來進行同步,Qt的事件循環會本身自動處理好這個。

 

 
class Dummy:public QObject 
{ 
    Q_OBJECT 
public: 
    Dummy(QObject* parent=0):QObject(parent)     {} 
public slots: 
    void emitsig() 
    { 
        emit sig(); 
    } 
signals: 
    void sig(); 
}; 
 
class Object:public QObject 
{ 
    Q_OBJECT 
public: 
    Object(){} 
public slots: 
    void slot() 
    { 
        qDebug()<<"from thread slot:" <<QThread::currentThreadId(); 
    } 
}; 
 
#include "main.moc" 
 
int main(int argc, char *argv[]) 
{ 
    QCoreApplication a(argc, argv); 
    qDebug()<<"main thread:"<<QThread::currentThreadId(); 
    QThread thread; 
    Object obj; 
    Dummy dummy; 
    obj.moveToThread(&thread); // 必須在對象的依附線程中執行此函數
    QObject::connect(&dummy, SIGNAL(sig()), &obj, SLOT(slot())); 
    thread.start(); 
    dummy.emitsig(); 
    return a.exec(); 
}

  執行結果:

main thread: 0x1a5c 
from thread slot: 0x186c

   確實簡單,只須要再object的子類中新建「耗時功能」的實現「便可,而後將此對象moveToThread 到線程對象便可。

2. QT多線程原則

  咱們能夠經過調用QObject::thread()能夠查詢一個QObject線程依附性

  注意,QCoreApplication對象以前建立的QObject沒有所謂線程依附性,所以也就沒有對象爲其派發事件。也就是說,實際是QCoreApplication建立了表明主線程的QThread對象

  

  咱們可使用線程安全的QCoreApplication::postEvent()函數向一個對象發送事件。它將把事件加入到對象所在的線程的事件隊列中,所以,若是這個線程沒有運行事件循環,即沒有依附的線程,這個事件也不會被派發。可是能夠經過將這種浮游對象經過QObject::moveToThread()來移入到一個已有的線程中,從而確保這些浮游的對象能夠依附線程。

   值得注意的一點是,雖然QObject是可重入的,可是 GUI 類,特別是QWidget及其全部的子類,都是否是可重入的。它們只能在主線程使用。因爲這些 GUI 類大都須要一個事件循環,因此,調用QCoreApplication::exec()也必須是主線程,不然這些 GUI 類就沒有事件循環了。你不能有兩個線程同時訪問一個QObject對象,除非這個對象的內部數據都已經很好地序列化(例如爲每一個數據訪問加鎖)。記住,在你從另外的線程訪問一個對象時,它可能正在處理所在線程的事件循環派發的事件!基於一樣的緣由,你也不能在另外的線程直接delete一個QObject對象,相反,你須要調用QObject::deleteLater()函數,這個函數會給對象所在線程發送一個刪除的事件。

(1)QObject的線程依附性是能夠改變的,方法是調用QObject::moveToThread()函數。該函數會改變一個對象及其全部子對象的線程依附性。因爲QObject不是線程安全的,因此咱們只能在該對象所在線程上調用這個函數。也就是說,咱們只能在對象所在線程將這個對象移動到另外的線程,不能在另外的線程改變對象的線程依附性。

(2)Qt 要求QObject的全部子對象都必須和其父對象在同一線程。這意味着:

  • 不能對有父對象(parent 屬性)的對象使用QObject::moveToThread()函數
  • 不能在QThread中以這個QThread自己做爲父對象建立對象,,這是由於要建立該線程對象必然在其餘的線程中建立,即該線程對象必然依附於其餘線程對象,而以該線程對象爲父類的子對向,在run函數中進行新建子類對象,若以其做爲父對象,則與QT所定義的原則衝突,所以禁止。
class Thread : public QThread {
    void run() {
        QObject *obj = new QObject(this); // 錯誤!
    }
};

  這是由於QThread對象所依附的線程是建立它的那個線程,而不是它所表明的線程。

 (3)Qt 還要求,在表明一個線程的QThread對象銷燬以前,全部在這個線程中的對象都必須先delete

  要達到這一點並不困難:咱們只需在QThread::run()棧空間(直接定義對象)上建立對象便可。

 

   如今的問題是,既然線程建立的對象都只能在函數棧上,怎麼能讓這些對象與其它線程的對象通訊呢?Qt 提供了一個優雅清晰的解決方案:咱們在線程的事件隊列中加入一個事件,而後在事件處理函數中調用咱們所關心的函數。顯然這須要線程有一個事件循環。這種機制依賴於 moc 提供的反射:所以,只有信號、槽和使用Q_INVOKABLE宏標記的函數能夠在另外的線程中調用。

   QMetaObject::invokeMethod()靜態函數會這樣調用:

QMetaObject::invokeMethod(object, "methodName",
                          Qt::QueuedConnection,
                          Q_ARG(type1, arg1),
                          Q_ARG(type2, arg2));

  上面函數調用中出現的參數類型都必須提供一個公有構造函數,一個公有的析構函數和一個公有的複製構造函數,而且要使用qRegisterMetaType()函數向 Qt 類型系統註冊。

  跨線程的信號槽也是相似的。當咱們將信號與槽鏈接起來時,QObject::connect()的最後一個參數將指定鏈接類型:

  • Qt::DirectConnection直接鏈接意味着槽函數將在信號發出的線程直接調用
  • Qt::QueuedConnection隊列鏈接意味着向接受者所在線程發送一個事件,該線程的事件循環將得到這個事件,而後以後的某個時刻調用槽函數
  • Qt::BlockingQueuedConnection阻塞的隊列鏈接就像隊列鏈接,可是發送者線程將會阻塞,直到接受者所在線程的事件循環得到這個事件,槽函數被調用以後,函數纔會返回
  • Qt::AutoConnection自動鏈接(默認)意味着若是接受者所在線程就是當前線程,則使用直接鏈接;不然將使用隊列鏈接;即若是接受者依附的線程就是當前線程,則直接鏈接,信號發出就調用。若是接受者依附的線程是其餘線程,則隊列鏈接,即向接受者所在線程發送一個事件,該線程的事件循環將得到這個事件,而後以後的某個時刻調用槽函數。

  注意在上面每種狀況中,發送者所在線程都是可有可無的!在自動鏈接狀況下,Qt 須要查看信號發出的線程是否是與接受者所在線程一致,來決定鏈接類型。注意,Qt 檢查的是信號發出的線程,而不是信號發出的對象所在的線程!咱們能夠看看下面的代碼:

class Thread : public QThread
{
Q_OBJECT
signals:
    void aSignal();
protected:
    void run() {
        emit aSignal();
    }
};
 
/* ... */
Thread thread;
Object obj;
QObject::connect(&thread, SIGNAL(aSignal()), &obj, SLOT(aSlot()));
thread.start();

  aSignal()信號在一個新的線程被髮出(也就是Thread所表明的線程)。注意,由於這個線程並非Object所在的線程(Object所在的線程和Thread所在的是同一個線程),可是aSignal()確實在Thread所表明的新線程中發出,所以,必然是隊列鏈接

3. 多線程的數據交互,數據同步問題

  線程對象依附的線程VS線程表明的新線程執行中對原有線程的訪問問題

class Thread : public QThread
{
Q_OBJECT
slots:
    void aSlot() {
        /* ... */
    }
protected:
    void run() {
        /* ... */
    }
};
 
/* ... */
Thread thread;
Object obj;
QObject::connect(&obj, SIGNAL(aSignal()), &thread, SLOT(aSlot()));
thread.start();
obj.emitSignal();

  這裏的obj發出aSignal()信號時,使用哪一種鏈接方式?答案是:直接鏈接。由於Thread對象所在線程發出了信號,也就是信號發出的線程與接受者是同一個。在aSlot()槽函數中,咱們能夠直接訪問Thread的某些成員變量,可是注意,在咱們訪問這些成員變量時,Thread::run()函數可能也在訪問!這意味着兩者併發進行這是一個完美的致使崩潰的隱藏bug

    

class Thread : public QThread
{
Q_OBJECT
slots:
    void aSlot() {
        /* ... */
    }
protected:
    void run() {
        QObject *obj = new Object;
        connect(obj, SIGNAL(aSignal()), this, SLOT(aSlot()));
        /* ... */
    }
};

  上面也是隊列鏈接,且Thread的aSlot槽函數依附於Thread對象的依附線程,即主線程。所以與子線程中的信號不在同一個線程中。

  若是爲了在子線程中調用線程對象自己的槽函數,且槽函數的執行也在該子線程中。則採用

1. 在線程的構造函數中使用QObject::moveToThread()方法

2. 直接指定鏈接方式爲 :Driect鏈接方式

3. 採用上文中推薦的方法。

 第一種:在線程構造函數中,QThread對象不是線程自己,將改對象依附到其本身所建立的線程中。

  實際上,這的確可行(由於Thread的線程依附性被改變了:它所在的線程成了本身),可是這並非一個好主意。這種代碼意味着咱們其實誤解了線程對象(QThread子類)的設計意圖:它們實際上是用於管理它所表明的線程的對象。所以,它們應該在另外的線程被使用(一般就是它本身所在的線程),而不是在本身所表明的線程中。

class Thread : public QThread {
Q_OBJECT
public:
    Thread() {
        moveToThread(this); // 錯誤!,不推薦
    }
 
    /* ... */
};

第二種:也不推薦

第三種:最好的解決方式,就是採用上面提到的,咱們能夠利用一個QObject的子類,使用QObject::moveToThread()改變其線程依附性:將處理任務的部分與管理線程的部分分離。

 

 

 

 

 

endl;

相關文章
相關標籤/搜索