一、信號槽的基本概念
關於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(this, new 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. };