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
對象之后,obj
、otherObj
和yetAnotherObj
的線程依附性是怎樣的?是不是就是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(); }
運行結果:槽函數在主線程中執行。
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;