Boost 的事件管理架構:Signal / Slot(中)

| | 0 Comments| 09:36
Categories:

延續前一篇,這篇繼續介紹 Boost::Signals2 這個函式庫關於連線管理的部分。

控制 slot 的順序

前面已經有提過了,一個 signal 可以連接多個 slot,而當這個 signal 被 emit 的時候,基本上在沒做特殊調整的狀況下,slot function 被執行的順序,是會按照 connect 的順序來執行的。而如果要指定 slot function 執行的順序的話,在 Boost 的 signals2 在 connect 的時候,是有提供一些方法可以做設定的。

Signals2 在執行 slot 的時候,實際上是分三個階段來執行的:

  1. 使用 boost::signals2::at_front 連結的 slot
  2. 指定群組(group)的 slot
  3. 使用 boost::signals2::at_back 連結的 slot

其中,有指定群組的 slot 又會再根據群組的順序來執行,所以可調整的空間算是相當地大。

使用方法的部分,這邊先針對 at_frontat_back 這兩種連結位置(connect position)來做說明。基本上,at_front 就是往前塞、越晚連接的越先執行,而 at_back 則是往後塞,越早連接的越先執行;而以 Signals2 來說,預設是使用 at_back,所以在沒有特殊定的情況下,救世會按照連接的順序來執行 slot。

要指定使用 at_frontat_back 當作連結位置也相當簡單,只要在執行 signal::connect() 的時候,除了傳入要連接的 slot 外,再額外加上指定的連結位置參數就可以了∼下面就是一個簡單的例子:

// include STL headers
#include <stdlib.h>
#include <iostream>

// include Boost header
#include <boost/signals2/signal.hpp>

// slot function
void slotFunc1(){ std::cout << "Function 1" << std::endl; }
void slotFunc2(){ std::cout << "Function 2" << std::endl; }
void slotFunc3(){ std::cout << "Function 3" << std::endl; }
void slotFunc4(){ std::cout << "Function 4" << std::endl; }
void slotFunc5(){ std::cout << "Function 5" << std::endl; }

int main( int argc, char** argv )
{
// create a signal
boost::signals2::signal<void () > mSignal1;

// connect signal and slot
mSignal1.connect( slotFunc1 );
mSignal1.connect( slotFunc2, boost::signals2::at_back );
mSignal1.connect( slotFunc3, boost::signals2::at_front );
mSignal1.connect( slotFunc4 );
mSignal1.connect( slotFunc5, boost::signals2::at_front );

// emit the signal
mSignal1();

return 0;
}

在這個範例裡,定義了五個 slot function:slotFunc1()slotFunc2()slotFunc3()slotFunc4()slotFunc5(),每一個 function 的內容,都只是輸出自己的編號而已,相當地單純。

而在連結部分的程式碼,這邊則是依序連結這五個 slot function,不過其中,slotFunc1()slotFunc4() 是使用一般的連結方法,也就是不特別指定連結位置(也就是採用預設值 at_back);而連結 slotFunc2() 的時候,雖然有強制指定為 at_back,不過實際上的意義和 1 和 4 是相同的。真正不一樣的,是在連結 slotFunc3()slotFunc5() 的時候,都指定了連結位置是 at_front

而這樣的執行結果,則是:

Function 5  <— front
Function 3 <— front
Function 1 <— back
Function 2 <— back
Function 4 <— back

其中,5 和 3 是 at_front 的,所以比較晚連結的 slotFunc5() 才會被第一個執行;而在這兩者執行後,才會再去執行 at_back 的 1、2、4,而這三個 slot function 就是按照連結的順序來執行了。

Slot 群組

除了 at_frontat_back 這兩種連結位置外,Boost::Signals2 也還提供了 slot 呼叫的群組功能,可以進一步地去調整 slot 被呼叫的順序。而要指定 slot 群組的方法也相當簡單,只要在呼叫 signal::connect() 時,在第一個參數指定群組(群組預設型別是 int)就可以了∼

而這邊的範例,就針對上面的程式碼「connect signal and slot」的部分做修改,變成:

// connect signal and slot
mSignal1.connect( slotFunc1 );
mSignal1.connect( 1, slotFunc2 );
mSignal1.connect( 0, slotFunc3 );
mSignal1.connect( 0, slotFunc4 );
mSignal1.connect( slotFunc5, boost::signals2::at_front );

其中,slotFunc1() 是使用預設的 at_back 作為連結位置,slotFunc5() 則是使用 at_front 當作連結位置;而其他的三個 slot 則是都有指定群組,其中 slotFunc2() 是指定群組「1」,slotFunc3()slotFunc4() 則是指定為群組「0」。如此一來,執行的結果就會變成是:

Function 5  <— front
Function 3 <— group 0
Function 4 <— group 0
Function 2 <— group 1
Function 1 <— back

他的執行順序,基本上就是先執行 at_frontslotFunc5(),再去執行群組裡的 slot,也就是 group 0 的 slotFunc3()slotFunc4(),以及 group 1 的 slotFunc2(),等這些都執行完後,最後執行 at_backslotFunc1()

而實際上,如果有需要的話,每一個 group 也都一樣可以指定 at_frontat_back 這兩種連結位置,在群組裡再做排序的控制,不過這邊就不舉例了。

最後補充說明一下,實際上 Signals2 的 slot group 型別,雖然預設是使用 int,但是實際上也是 template、可以自己去定義的∼有興趣的話,可以去參考「boost/signals2/preprocessed_signal.hpp」這個檔案(連結)裡面針對 boost::signals2::signal 這個 template class 的定義,應該就知道該怎麼指定群組的型別了∼

中斷連線

雖然一般的狀況下,在連接 signal 和 slot 後,就不會再去動這個連結關係了,但是某些情況下,可能還是會需要解除連結關係。這時候,Boost::Signals2 提供了幾種作法,可以中斷 singal 和 slot 之間的關係。

  • 使用 signal::disconnect_all_slots() 中斷所有連結

    這是一個最簡單的方法,只要執行了 signal 的 disconnect_all_slots() 這個成員函式,他就會把所有已經建立的連結都中斷掉。例如在上面的例子裡,如果加入一行「mSignal1.disconnect_all_slots();」,那之後再去執行 mSignal1() 時,就不會再呼叫到任何 slot function 了∼

  • 使用 signal::disconnect() 中斷指定的連結

    上面的方法是中斷所有的連結,而如果只是要中斷特定的連結的話,也可以透過 disconect() 這個函式來做到。他有兩個版本,第一個版本是指定要中斷和哪一個 slot 的連結、第二個則是指定要中斷哪一個 slot group 的連結。

    以上面的範例程式來說,只要執行了「mSignal1.disconnect( slotFunc1 )」,就可以中斷 mSignal1 這個 signal 和 slotFunc1() 之間的連結;而如果執行「mSignal1.disconnect( 0 )」的話,則可以中斷mSignal1 group 0 這個 slot group 裡的所有 slot 的連結,包括了 slotFunc3()slotFunc4()

    透過這兩種使用方法,基本上應該就可以應付大部分要切斷連結的狀況了∼不過要注意的是,其實 Signals2 的 signal 是允許同一個 signal 重複連結到同一個 slot 的;而在重複連結的情況下,同一個 slot function 也會被重複呼叫。相對於此,disconnect() 則是會將所有符合條件的連結都中斷掉,這是在使用上可能要注意的。下面是一個簡單的例子:

    // 1. connect signal and slot
    mSignal1.connect( slotFunc1 );
    mSignal1.connect( slotFunc1 );
    mSignal1.connect( slotFunc2 );

    // 2. emit the signal
    std::cout << "Emit signal" << std::endl;
    mSignal1();

    // 3. disconnect slotFunc1
    mSignal1.disconnect( slotFunc1 );

    // 4. emit the signal
    std::cout << "Emit signal after disconnect slotFunc1()" << std::endl;
    mSignal1();

    這個程式碼的執行結果,會是下面這樣子:

    Emit signal
    Function 1
    Function 1
    Function 2
    Emit signal after disconnect slotFunc1()
    Function 2

    可以發現,在第一次 emit 這個 signal 的時候,slotFunc1() 由於被連結了兩次,所以也被執行了兩次;但是在 mSignal1.disconnect( slotFunc1 ) 過後,再去 emit 這個 signal 的話,由於 slotFunc1() 的兩個連結都被中斷了,所以就剩下 slotFunc2() 會被執行到了。

  • 透過 boost::signals2::connection 中斷指定的連結

    除了透過 signal 的成員函式來進行中斷連線之外,其實也可以透過 Boost::Signals2 提供了 boost::signals2::connection 型別的物件,來做個別連線的管理;它的使用方法大致如下:

    // 1. connect signal and slot
    boost::signals2::connection c1, c2, c3;
    c1 = mSignal1.connect( slotFunc1 );
    c2 = mSignal1.connect( slotFunc1 );
    c3 = mSignal1.connect( slotFunc2 );

    // 2. emit the signal
    std::cout << "Emit signal" << std::endl;
    mSignal1();

    // 3. disconnect the first connection
    c1.disconnect();

    // 4. emit the signal
    std::cout << "Emit signal after disconnect 1st connection" << std::endl;
    mSignal1();

    在上面的程式碼裡可以看到,實際上在透過 signal::connect() 建立 signal 和 slot 間的連結的時候,都會傳回一個型別是 boost::signals2::connection 的物件,而這個物件就是用來做 signal 和 slot 之間,個別連結的管理的;如果要切斷個別的連結的話,就只要呼叫他的 disconnect() 函式就可以了∼

    像以上面的程式碼在執行後,結果就會是:

    Emit signal
    Function 1
    Function 1
    Function 2
    Emit signal after disconnect 1st connection
    Function 1
    Function 2

    可以發現,透過這樣來中斷連結的話,就不會像使用 signal::connect() 一樣,把重複的連結也都中斷,而可以只中斷自己想要的連結了∼不過相對的,要用這東西的話,就還得自己額外去管理這些 connection 的物件,會比較麻煩就是了。

暫時停止連線(block)

上面一個段落,主要是在講要如何中斷一個連線;但是其實在某些時候,我們需要的不是永遠中斷這些連線,而只是需要暫時停止這些連線(一般是稱為「block」,通常是為了怕 signal / slot 造成無窮迴圈),這時候該怎麼辦呢?Signals2 為了這種狀況,提供了 boost::signals2::shared_connection_block,讓程式設計師可以來做連線狀態的管理;它的使用方法大致會像下面這個樣子:

// include STL headers
#include <stdlib.h>
#include <iostream>

// include Boost header
#include <boost/signals2/signal.hpp>
#include <boost/signals2/shared_connection_block.hpp>

// slot function
void slotFunc1(){ std::cout << "Function 1" << std::endl; }
void slotFunc2(){ std::cout << "Function 2" << std::endl; }

int main( int argc, char** argv )
{
// create signal
boost::signals2::signal<void ()> mSignal;

// connect signal and slot
boost::signals2::connection c1 = mSignal.connect( slotFunc1 ),
c2 = mSignal.connect( slotFunc2 );
{
// block the connection in this scope
boost::signals2::shared_connection_block block( c1 );
// emit the signal
std::cout << "C1 is blocked" << std::endl;
mSignal();
}


// emit the signal
std::cout << "unblock scope" << std::endl;
mSignal();

return 0;
}

上面的執行結果,會是:

C1 is blocked
Function 2
unblock scope
Function 1
Function 2

首先,和 signals2 裡面其他型別不太一樣,要使用 shared_connection_block 這個型別必須要額外 include「boost/signals2/shared_connection_block.hpp」這個檔案才可以使用。而在使用時,他必須要有本來連結的 connection,才能建立、操作;所以在這裡,用 c1c2 來個別紀錄 mSignalslotFunc1() 以及 slotFunc2() 的連線。

而在上面程式碼猶大括號({ })圈起來的黃底區域(scope、生存空間)裡面,就是建立了一個型別是 boost::signals2::shared_connection_block 的物件 block,並且將要 block 的連線 c1 傳入,讓他知道要管理的連線。而在這樣建立 block 這個物件後,c1 這個連線就已經被 block 掉了∼所以在之後 emit mSignal 的時候,只會執行到 slotFunc2() 這個函式、而不會執行 slotFunc1() 的內容。

不過當出了黃色區域這個 scope 後,block 這個物件就會消失了,而在他消失前,會把它 block 掉的連線還原,所以之後再去 emit mSignal 的時候,就會連 slotFunc1() 一起執行了。

雖然這邊只是單純透過 scope 來 block signal 和 slot 連線,不過實際上,shared_connection_block 的物件也具有 block()unblock() 這兩個成員函式,可以用來控制是否要 block 掉 signal 和 slot 間的連線。所以有需要的話,也是可以自己去透過管理一份 shared_connection_block 的物件,來做 block 的控制。

話說,本來只打算分上下兩篇的,不過後來看來要把剩下想寫的東西都塞到這篇好像會變得太長,所以還是決定再拆一篇出來,專門來講自動連線管理好了。

Leave a Reply

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *