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
對象以後,obj
、otherObj
和yetAnotherObj
的線程依附性是怎樣的?是否是就是MyThread
所表示的那個線程?要回答這個問題,咱們必須看看到底是哪一個線程建立了它們:實際上,是調用了MyThread
構造函數的線程建立了它們。所以,這些對象不在MyThread
所表示的線程,而是在建立了MyThread
的那個線程中。 this
(1)QObject::connectspa
涉及信號槽,咱們就躲不過 connect 函數,只是這個函數你們太熟悉。我很差意思再用一堆廢話來描述它,但不說又不行,那麼折中一下,只看它的最後一個參數吧(爲了簡單起見,只看它最經常使用的3個值):
經過指定connect的鏈接方式,若是指定直接鏈接(Direct Connection),則該槽函數將再信號發出的線程中直接執行,而不用斷定當前信號發出的線程與槽函數所在線程的狀態;若是指定隊列鏈接(Queued Connection),則該槽函數在接受者所依附的線程的線程循環中被指定調用;若是爲自動鏈接(Auto Connection)須要斷定發射信號的線程和接受者所依附的線程是否相同,進行細分指定。
(2)qT線程管理的原則:
- QThread 是用來管理線程的,它所依附的線程和它管理的線程並非同一個東西
- QThread 所依附的線程,就是執行 QThread t 或 QThread * t=new QThread 所在的線程。也就是我們這兒的主線程
- QThread 管理的線程,就是 run 啓動的線程。也就是次線程
- 由於QThread的對象依附在主線程中,所以他的slot函數會在主線程中執行,而不是次線程。除非:
- QThread 對象依附到次線程中(經過movetoThread)
- slot 和信號是直接鏈接(經過connect鏈接方式來指定),且信號在次線程中發射
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 強烈批判的用法。不推薦這樣使用。
即,信號在子線程發射,而槽函數誰線程的槽函數,而線程對象在主線程中建立,若是連接採用自動連接,則條件判斷比爲隊列鏈接,且由主線程在主線程的事件循環中執行。以下所示:
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
的全部子對象都必須和其父對象在同一線程。這意味着:
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;