1. 程式人生 > >QT中的線程與事件循環理解(2)

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

註釋 由於 做什麽 hid post 適用於 tsig call 循環

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中說的清楚:

run 對於線程的作用相當於main函數對於應用程序。它是線程的入口,run的開始和結束意味著線程的開始和結束。

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的構造函數中構建的對象:

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

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

  在我們創建了MyThread對象之後,objotherObjyetAnotherObj的線程依附性是怎樣的?是不是就是MyThread所表示的那個線程?要回答這個問題,我們必須看看究竟是哪個線程創建了它們:實際上,是調用了MyThread構造函數的線程創建了它們。因此,這些對象不在MyThread所表示的線程,而是在創建了MyThread的那個線程中  

(1)QObject::connect

   涉及信號槽,我們就躲不過 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;

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