Qt訊號槽的一些事 Qt::帶返回值的訊號發射方式
一般來說,我們發出訊號使用emit這個關鍵字來操作,但是會發現,emit並不算一個呼叫,所以它沒有返回值。那麼如果我們發出這個訊號想獲取一個返回值怎麼辦呢?
兩個辦法:1.通過出參形式返回,引用或者指標的方式帶回;比如emit sig(int& i)或者emit sig(void* pointer),但是這個方法有一個弊端,稍後介紹第二種方式會提醒。
2.通過qt自帶的invoke機制呼叫:參考文件對QMetaObject::invokeMethod的說明:Invokes the member (a signal or a slot name) on the object obj.也就是說回撥是可以回撥訊號或者槽的。一般來說,我們使用invokeMethod是在子執行緒需要排程UI操作的時候(已經有很多文章詳細說明了使用方式,不再贅述),因為UI操作只能在主執行緒中使用(否則會出現未定義錯誤),通過這種回撥方式,讓要操作的事件回到主執行緒時間片的時候再來執行。大部分情況下,我們把UI操作封裝在一個槽裡,用回撥方式來排程。同樣訊號也可以用這種方式,但是有幾點需要注意的是,1.呼叫回撥的連線方式:如果訊號和連線槽在一個執行緒內,那麼必須用Qt::DirectConnection或者Qt::AutoConnection,這樣的話,保證訊號回撥後,執行緒會等待訊號連線槽執行完畢,才可能取到我們需要的返回值;如果使用了Qt::QueuedConnection,那麼訊號只是負責把事件交給事件佇列,然後馬上做出返回,這樣,是否有返回值就無法確定了(這也就是第一個方法的弊端,因為訊號發射是根據訊號和槽各自的執行緒情況來選擇的連線方式).如果訊號和槽在兩個執行緒中,那麼首先肯定不能使用Qt::DirectConnection,除非你很清楚連線槽的動作是否保證了執行緒安全。但根據第一條的說明,也不能使用Qt::QueuedConnection。不過還好qt提供了一個額外的連線方式就是Qt::BlockingQueuedConnection,這個連線方式會阻塞住發射訊號的執行緒一直等到佇列連線槽返回後,才會恢復阻塞,這樣就可以保證我們能拿到真正的返回值。(但是使用這種方式需要你清楚的知道,發射執行緒是否允許阻塞和連線槽是否對這個阻塞執行緒有什麼特別的操作,一般來說,如果這個執行緒並不是由你自己控制的話,不要隨便嘗試去阻塞別人的執行緒,因為你並不清楚別人執行緒的執行邏輯)
呼叫方式大致程式碼如下bool bReturn; QMetaObject::invokeMethod(&object, "sig", Qt::DirectConnection/*Qt::QueuedConnection*/, Q_RETURN_ARG(bool, bReturn), Q_ARG(int, i));
https://github.com/KaiMingPrince注:此文是站在Qt5的角度說的,對於Qt4部分是不適用的。
1.先說Qt訊號槽的幾種連線方式和執行方式。
1)Qt訊號槽給出了五種連線方式:
Qt::AutoConnection | 0 | 自動連線:預設的方式。訊號發出的執行緒和糟的物件在一個執行緒的時候相當於:DirectConnection, 如果是在不同執行緒,則相當於QueuedConnection |
Qt::DirectConnection | 1 | 直接連線:相當於直接呼叫槽函式,但是當訊號發出的執行緒和槽的物件不再一個執行緒的時候,則槽函式是在發出的訊號中執行的。 |
Qt::QueuedConnection | 2 | 佇列連線:內部通過postEvent實現的。不是實時呼叫的,槽函式永遠在槽函式物件所在的執行緒中執行。如果訊號引數是引用型別,則會另外複製一份的。執行緒安全的。 |
Qt::BlockingQueuedConnection | 3 | 阻塞連線:此連線方式只能用於訊號發出的執行緒(一般是先好物件的執行緒) 和 槽函式的物件不再一個執行緒中才能用。通過訊號量+postEvent實現的。不是實時呼叫的,槽函式永遠在槽函式物件所在的執行緒中執行。但是發出訊號後,當前執行緒會阻塞,等待槽函式執行完畢後才繼續執行。 |
Qt::UniqueConnection | 0x80 | 防止重複連線。如果當前訊號和槽已經連線過了,就不再連線了。 |
2)訊號槽的呼叫方式和執行緒:
UniqueConnection 模式:嚴格說不算連線方式,方式就是4中,此只是一個附加的引數。不討論。
AutoConnection 模式:這個模式是預設的,但其可以看作是DirectConnection和QueuedConnection的自動選擇,直接分析那兩種也就行了。
發出訊號,呼叫槽的方式也可以簡單的分為兩種:同步呼叫和非同步呼叫
同步呼叫:發出訊號後,當前執行緒等待槽函式執行完畢後才繼續執行。
非同步呼叫:發出訊號後,立即執行剩下邏輯,不關心槽函式什麼時候執行。
所以有下表:
執行緒/模式 | DirectConnection | QueuedConnection | BlockingQueuedConnection |
相同執行緒 | 直接呼叫,同步呼叫。 | 通過事件進行佇列呼叫。非同步呼叫. | 不可用 |
不同執行緒 | 直接呼叫。同步呼叫。槽函式在發出訊號的執行緒執行。有執行緒安全隱患。 | 通過事件進行佇列呼叫。非同步呼叫.槽函式在物件所在的執行緒執行。執行緒安全。 | 通過事件進行阻塞呼叫。同步呼叫。槽函式在物件所在的執行緒執行。執行緒安全。 |
Qt事件迴圈依賴 | 直接呼叫,不依賴Qt事件迴圈 | 通過事件進行佇列呼叫。依賴,槽函式所在物件的執行緒必須啟用Qt事件迴圈 | 通過事件進行佇列呼叫,用訊號量實現阻塞。依賴,槽函式所在物件的執行緒必須啟用Qt事件迴圈 |
2.Qt訊號連線多個槽,呼叫順序。
先說基本原則:
槽函式開始呼叫的順序和連線的順序是一致的。
但是,上面也說了,有同步呼叫和非同步呼叫。
對於同步呼叫,你觀察的結果和基本原則一樣。
但是對於非同步呼叫,可能你最先連線的它,但是可能其他都執行完畢了,但是其還沒執行。是因為對於非同步呼叫:是開始呼叫的時候,生成一個需要呼叫這個函式的事件,然後放到事件佇列裡。然後立即返回,去執行呼叫其他槽函式或者槽函式都執行了,不關心槽函式的執行狀態的。等到事件佇列裡任務輪到此事件再去呼叫。
3.訊號的返回值。
大都說Qt訊號槽不能使用返回值。其實不不準確的,Qt5中,訊號槽是有返回值的。只是Qt的一個訊號可以連線多個槽,還有同步呼叫和非同步呼叫的問題,沒發支援的很好,所以,返回值雖有,但只是雞肋。
先說下返回值的規則把:
- 同步呼叫才有返回值,非同步呼叫的返回值永遠為返回值型別預設構造函數出來的。
- 連線的多個槽都返回值,那麼結果是最後呼叫(連線)的那個。
也就是說對於QueuedConnection連線的訊號槽,永遠只是返回返回型別的預設建構函式的。對於AutoConnection連線的,如果發出訊號的執行緒和槽函式執行緒不同亦然。
測試小例子地址:https://github.com/dushibaiyu/DsbyLiteExample/tree/master/QtSignalsSlotTest
4.訊號引數的安全問題:
因為一個訊號可以連線多個槽函式,如果引數是T * 或者是T &話會不會第一個槽函式改變引數的值,然後第二此呼叫的引數就已經不是訊號發出的值?
1)對於T &: 在同步呼叫中則是變化的,不可用於非同步,不可跨執行緒。所以BlockingQueuedConnection方式的同步也不行。(T& 不可用在佇列呼叫(QueuedConnection)和阻塞呼叫(BlockingQueuedConnection)中。只能使用const T &。)
因為同步呼叫,你可以理解成直接呼叫,那麼連線多個槽函式就相當於直接連續呼叫多個函式。類似於:
// 函式原型都是:void (int &a ) int a; fun1(a); fun2(a) ····· // 函式原型都是:void (int * a ) int a; pfun1(&a); pfun2(&a) ·····
這樣,當第一個函式執行改變引數值之後,其後的函式呼叫都要受影響。
2) 對於T *,最好不要同時連線多個槽。
對於同步呼叫:是一個接著一個呼叫的,執行順序類似上面,所以值也是每次呼叫也會變化的。
對於非同步呼叫:其內容確實不確定的,因為非同步呼叫的時間是不可控的。如果還有跨執行緒相關,則還有執行緒安全問題。
5.訊號槽效能損失:
注:僅僅程式碼層進行的理論分析,非實際測試,不嚴謹,不權威。
關於訊號槽(很多吐槽Qt就是說的這個):
(1)Qt4語法的,都說是匹配字串,其實只是連結訊號槽的用的匹配字串 的方法,通過字串找到訊號和槽在QMeatObject裡存的索引位置int型別,還有槽函式的索引,然後呼叫的時候通過索引號用switch去區分的 發射的那個函式,然後取出對應的連結槽的list,迴圈檢測槽函式的引數是否匹配,然後呼叫槽函式。。這個連結時會耗時查詢,但是你能有多少訊號?這個鏈 接也耗時不多,呼叫的時候耗時主要就是在引數匹配上了。
(2)Qt5 語法的,Qt5 的槽函式連結和執行是基於模板實現的,函式物件。訊號和槽的引數問題是編譯時檢查的,執行效率更高,但是編譯就慢點了。連結時也是通過訊號的地址找到其的 訊號索引,至於槽函式直接是生成一個函式物件的,然後呼叫的時候也是先switch找到發射的訊號,取出list,然後逐個呼叫其儲存的函式物件,所以對 於Qt5 語法的訊號槽,呼叫效能損失幾乎可以說無的。
(3)連結的訊號槽的時候,Qt::UniqueConnection的連結方式會對已經連結過的此先好的槽函式進行遍歷,會有連結時的損失。其他連結的損失就在上面說過了。
(3)在訊號槽呼叫的時候,還有一些連結方式和執行緒的判斷和為了安全問題的鎖操作。關於這個就還涉及到呼叫槽函式的執行緒問題。
對於同線程直接呼叫,較函式物件直接呼叫的損失,就只有連結方式和執行緒的判斷的幾個if 分支和 鎖的操作。
對於執行緒間通訊的呼叫,跨執行緒。訊號槽內部也是通過Qt事件迴圈機制實現的,跨執行緒就不是時時呼叫了,主要是安全了,對於效能有沒有損失沒法評論的。對於跨執行緒阻塞的呼叫,這個也是事件實現,只是但發射訊號的執行緒會阻塞,這個找不到對應的直接呼叫的比較,也不好說。
關於訊號槽Qt是作何很多方便使用和安全呼叫,較之函式指標,效能會有損失,但是也沒損失多少的。對於函式物件呼叫,Qt5語法的呼叫,幾乎是不損失什麼的。
注:此文是個人根據文件,原始碼和自己寫小例子測試得出的總結,如有錯誤請您指出。
一般來說,我們發出訊號使用emit這個關鍵字來操作,但是會發現,emit並不算一個呼叫,所以它沒有返回值。那麼如果我們發出這個訊號想獲取一個返回值怎麼辦呢?
兩個辦法:1.通過出參形式返回,引用或者指標的方式帶回;比如emit sig(int& i)或者emit sig(void* pointer),但是這個方法有一個弊端,稍後介紹第二種方式會提醒。
2.通過qt自帶的invoke機制呼叫:參考文件對QMetaObject::invokeMethod的說明:Invokes the member (a signal or a slot name) on the object obj.也就是說回撥是可以回撥訊號或者槽的。一般來說,我們使用invokeMethod是在子執行緒需要排程UI操作的時候(已經有很多文章詳細說明了使用方式,不再贅述),因為UI操作只能在主執行緒中使用(否則會出現未定義錯誤),通過這種回撥方式,讓要操作的事件回到主執行緒時間片的時候再來執行。大部分情況下,我們把UI操作封裝在一個槽裡,用回撥方式來排程。同樣訊號也可以用這種方式,但是有幾點需要注意的是,1.呼叫回撥的連線方式:如果訊號和連線槽在一個執行緒內,那麼必須用Qt::DirectConnection或者Qt::AutoConnection,這樣的話,保證訊號回撥後,執行緒會等待訊號連線槽執行完畢,才可能取到我們需要的返回值;如果使用了Qt::QueuedConnection,那麼訊號只是負責把事件交給事件佇列,然後馬上做出返回,這樣,是否有返回值就無法確定了(這也就是第一個方法的弊端,因為訊號發射是根據訊號和槽各自的執行緒情況來選擇的連線方式).如果訊號和槽在兩個執行緒中,那麼首先肯定不能使用Qt::DirectConnection,除非你很清楚連線槽的動作是否保證了執行緒安全。但根據第一條的說明,也不能使用Qt::QueuedConnection。不過還好qt提供了一個額外的連線方式就是Qt::BlockingQueuedConnection,這個連線方式會阻塞住發射訊號的執行緒一直等到佇列連線槽返回後,才會恢復阻塞,這樣就可以保證我們能拿到真正的返回值。(但是使用這種方式需要你清楚的知道,發射執行緒是否允許阻塞和連線槽是否對這個阻塞執行緒有什麼特別的操作,一般來說,如果這個執行緒並不是由你自己控制的話,不要隨便嘗試去阻塞別人的執行緒,因為你並不清楚別人執行緒的執行邏輯)
呼叫方式大致程式碼如下bool bReturn; QMetaObject::invokeMethod(&object, "sig", Qt::DirectConnection/*Qt::QueuedConnection*/, Q_RETURN_ARG(bool, bReturn), Q_ARG(int, i));