【Qt】connect機制原理

陸人葭發表於2016-09-24
一、訊號槽的基本概念
關於QT訊號槽的基本概念大家都懂,通過訊號槽機制,QT使物件間的通訊變得非常簡單:
A物件宣告訊號(signal),B物件實現與之引數相匹配的槽(slot),通過呼叫connect進行連線,合適的時機A物件使用emit把訊號帶上引數發射出去,B物件的槽會就接收到響應。
 
訊號槽機制有一些特點:
1.   型別安全:只有引數匹配的訊號與槽才可以連線成功(訊號的引數可以更多,槽會忽略多餘的引數)。
2.   執行緒安全:通過藉助QT自已的事件機制,訊號槽支援跨執行緒並且可以保證執行緒安全。
3.   鬆耦合:訊號不關心有哪些或者多少個物件與之連線;槽不關心自己連線了哪些物件的哪些訊號。這些都不會影響何時發出訊號或者訊號如何處理。
4.   訊號與槽是多對多的關係:一個訊號可以連線多個槽,一個槽也可以用來接收多個訊號。
 
使用這套機制,類需要繼承QObject並在類中宣告Q_OBJECT。下面就對訊號槽的實現做一些剖析,瞭解了這些在使用的時候就不會踩坑嘍。
二、訊號與槽的定義
槽:用來接收訊號,可以被看作是普通成員函式,可以被直接呼叫。支援public,protected,private修飾,用來定義可以呼叫連線到此槽的範圍。
1. public slots:  
2.     void testslot(const QString& strSeqId);  
訊號:只需要宣告訊號名與引數列表即可,就像是一個只有宣告沒有實現的成員函式。
1. signals:  
2.     void testsignal(const QString&); 
QT會在moc的cpp檔案中實現它(參考下面程式碼)。下面程式碼中呼叫activate的第三個引數是類中訊號的序列號。
1. // SIGNAL 0  
2. void CTestObject:: testsignal (const QString & _t1)  
3. {  
4.     void *_a[] = { 0, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) };  
5.     QMetaObject::activate(this, &staticMetaObject, 0, _a);  
6. }  
三、訊號槽的連線與觸發
通過呼叫connect()函式建立連線,會把連線資訊儲存在sender物件中;呼叫desconnect()函式來取消。
connect函式的最後一個引數來用指定連線型別(因為有預設,我們一般不填寫),後面會再提到它。
1. static bool connect(const QObject *sender, const QMetaMethod &signal,  
2.     const QObject *receiver, const QMetaMethod &method,  
3.     Qt::ConnectionType type = Qt::AutoConnection);  
一切就緒,發射!在sender物件中呼叫:
1. emit testsignal(“test”);  
1. # define emit  
上面程式碼可以看到emit被定義為空,這樣在發射訊號時就相當於直接呼叫QT為我們moc出來的函式testsignal(constQString & _t1)。
具體的操作由QMetaObject::activate()來處理:遍歷所有receiver並觸發它們的slots。針對不同的連線型別,這裡的派發邏輯會有不同。
四、不同的連線型別剖析
QueuedConnection:向receiver所線上程的訊息迴圈傳送事件,此事件得到處理時會呼叫slot,像Win32的::PostMessage。
BlockingQueuedConnection:處理方式和QueuedConnection相同,但傳送訊號的執行緒會等待訊號處理結束再繼續,像Win32的::SendMessage。
DirectConnection:在當前執行緒直接呼叫receiver的slot,這種型別無法支援跨執行緒的通訊。
AutoConnection:當前執行緒與receiver執行緒相同時,直接呼叫slot,否則同QueuedConnection型別。
 
1. QObject * const receiver = c->receiver;  
2. const bool receiverInSameThread = currentThreadId == receiver->d_func()->threadData->threadId;  
3.   
4. // determine if this connection should be sent immediately or  
5. // put into the event queue  
6. if ((c->connectionType == Qt::AutoConnection && !receiverInSameThread)  
7.     || (c->connectionType == Qt::QueuedConnection)) {  
8.         queued_activate(sender, signal_absolute_index, c, argv ? argv : empty_argv);  
9.         continue;  
10. #ifndef QT_NO_THREAD  
11. else if (c->connectionType == Qt::BlockingQueuedConnection) {  
12.     locker.unlock();  
13.     if (receiverInSameThread) {  
14.         qWarning("Qt: Dead lock detected while activating a BlockingQueuedConnection: "  
15.             "Sender is %s(%p), receiver is %s(%p)",  
16.             sender->metaObject()->className(), sender,  
17.             receiver->metaObject()->className(), receiver);  
18.     }  
19.     QSemaphore semaphore;  
20.     QCoreApplication::postEvent(receiver, new QMetaCallEvent(c->method_offset, c->method_relative,  
21.         c->callFunction,  
22.         sender, signal_absolute_index,  
23.         0, 0,  
24.         argv ? argv : empty_argv,  
25.         &semaphore));  
26.     semaphore.acquire();  
27.     locker.relock();  
28.     continue;  
29. #endif  
30. }  
31.  
32. // 接下來的程式碼會直接在當前執行緒呼叫receiver的slot函式
五、QT物件所屬執行緒的概念
這裡要引入QObject的所屬執行緒概念,看一下QObject的建構函式(隨便選擇一個過載)就一目瞭然了。
如果指定父物件並且父物件的當前執行緒資料有效,則繼承,否則把建立QObject的執行緒作為所屬執行緒。
1. QObject::QObject(QObject *parent)  
2.     : d_ptr(new QObjectPrivate)  
3. {  
4.     Q_D(QObject);  
5.     d_ptr->q_ptr = this;  
6.     d->threadData = (parent && !parent->thread()) ? parent->d_func()->threadData : QThreadData::current();  
7.     d->threadData->ref();  
8.     if (parent) {  
9.         QT_TRY {  
10.             if (!check_parent_thread(parent, parent ? parent->d_func()->threadData : 0, d->threadData))  
11.                 parent = 0;  
12.             setParent(parent);  
13.         } QT_CATCH(...) {  
14.             d->threadData->deref();  
15.             QT_RETHROW;  
16.         }  
17.     }  
18.     qt_addObject(this);  
19. }  
 
通過activate()的程式碼可以看到,除了訊號觸發執行緒與接收者執行緒相同的情況能直接呼叫到slot,其它情況都依賴事件機制,也就是說receiver執行緒必須要有QT的eventloop,否則slot函式是沒有機會觸發的!
當我們奇怪為什麼訊號發出slot卻不被觸發時,可以檢查一下是否涉及到跨執行緒,接收者的執行緒是否存在啟用的eventloop。
所幸,我們可以通過呼叫QObject的方法movetothread,來更換物件的所屬執行緒,將有需求接收訊號的物件轉移到擁有訊息迴圈的執行緒中去以確保slot能正常工作。
 
有一個和物件所屬執行緒相關的坑:QObject::deletelater() 。從原始碼可以看出,這個呼叫也只是傳送了一個事件,等物件所屬執行緒的訊息迴圈獲取控制權來處理這個事件時做真正的delete操作。
所以呼叫這個方法要謹慎,確保物件所屬執行緒具有啟用的eventloop,不然這個物件就被洩露了!
 
1. void QObject::deleteLater()  
2. {  
3.     QCoreApplication::postEvent(thisnew QEvent(QEvent::DeferredDelete));  
4. }  
六、強制執行緒切換
當物件中的一些介面需要確保在具有訊息迴圈的執行緒中才能正確工作時,可以在介面處進行執行緒切換,這樣無論呼叫者在什麼執行緒都不會影響物件內部的操作。
下面的類就是利用訊號槽機制來實現執行緒切換與同步,所有對testMethod()的呼叫都會保證執行在具有事件迴圈的執行緒中。
1. class CTestObject : public QObject  
2. {  
3.     Q_OBJECT  
4.   
5. public:  
6.     CTestObject(QObject *parent = NULL)  
7.         : QObject(parent)  
8.     {  
9.         // 把自己轉移到帶有事件迴圈的QThread  
10.         this->moveToThread(&m_workThread);  
11.   
12.         // 外部呼叫一律通過訊號槽轉移到物件內部的工作執行緒  
13.         // 連線型別選擇為Qt::BlockingQueuedConnection來達到同步呼叫的效果  
14.        connect(this, SIGNAL(signalTestMethod(const QString &)), this, SLOT(slotTestMethod(const QString &)), Qt::BlockingQueuedConnection);   
15.           
16.         m_workThread.start();  
17.     }  
18.     ~CTestObject();  
19.   
20.     void testMethod(const QString& strArg)  
21.     {  
22.         if (QThread::currentThreadId() == this->d_func()->threadData->threadId)  
23.         {  
24.             // 如果呼叫已經來自物件所屬執行緒,直接處理  
25.             slotTestMethod(strArg);  
26.         }   
27.         else  
28.         {  
29.             // 通過傳送訊號,實現切換執行緒處理  
30.             emit signalTestMethod(strArg);  
31.         }  
32.     }  
33.   
34. signals:  
35.     void signalTestMethod(const QString&);  
36.   
37. private slots:  
38.     void slotTestMethod(const QString& strArg)  
39.     {  
40.         // 方法的具體實現  
41.     }  
42.   
43. private:  
44.     QThread                     m_workThread;  
45. };  v

相關文章