理解alivc_framework狀態機

cheenc發表於2019-02-14

背景:

對與現在元件狀態現狀以及轉換過程有點疑惑,自己邊熟悉程式碼邊思考。c++標準庫以及Boost庫並沒有提供這種關於狀態機類似的基類别範本,原因是因為外面類複雜多樣,不同類的狀態沒有共性。但是聚焦到音視訊模組,這些類有著類似的特性,因此我們可以簡單的總結和基類化一些基類别範本,供模組繼承使用。

正文:

這裡指的模組類,一般是比較大的功能模組的介面類或者代理類(常見的如decode,encode等)。一般下面有對應的impl實現類,功能附帶緩衝類等。由於是模組類是一個模組整體對外的表現,因此介面,狀態也比較多。因此從上層角度出發,統一規劃所有模組類進行統一表現,是個比較好的實現。同時也經常會遇到特殊不符合統一的設計的情況。需要特殊特化處理。

1.個人理解模組狀態分類:

a)構造態,(未初始化態uninit

模組類一般比較大,成員較多。個人習慣建構函式不進行業務處理,不傳引數,不拋異常,僅僅對模組類內部成員進行引用,bind等互相關聯。完成構造過程。

b)初始化態,(inited

一般是外面呼叫,如果僅僅是自己建構函式呼叫就比較雞肋了(還不如寫到建構函式裡面)。可以接受外部傳引數。根據外部引數返回Init成功或者失敗,或者拋異常,這都可行的。

c) 執行態,(run/start

一般是外部呼叫,外面再初始化設定引數,或者初始化前後設定引數,開始執行。嚴格意義來說執行時期是不接受引數改變的(部分特殊支援執行態改引數也是可以的)。

d) 暫停態,(pause)

個人理解這也是非執行態,只不過保留了執行態的上下文而已。

以上就是我覺得一個元件必須有的4個必要的狀態,或者說是使用者必須需要關心的狀態。說明一點的就是,這裡是非同步的情況,當然如果有些同步元件設計比較簡單,比如是同步的介面,就是 init/doTask/uninit 就完成了工作,那肯定就沒有執行態和暫停態了。但是考慮到模組都是負責比較複雜的業務,如果是純同步的情況沒必要設計成模組,或者說純同步的模組實在不多我們暫不考慮。

2.模組切換函式

  1. constructor:模組肯定是由constructor構造了。這裡值得說明一下的就是,如果該模組是Service型別的,也就是說全域性單例,則建構函式不是由業務邏輯控制的。這種單例的情況,般預設認為已經構造好了。一般是通過靜態函式getInstance來獲取類即可。
  2. initpara:通過呼叫init函式,實現uninit => inited 狀態切換,如果是service,應該提供初始化形參,service返回初始化上下文context.
  3. start():通過呼叫start,實現inited=>running 狀態切換
  4. pause():通過呼叫pause,實現running=>pause狀態切換。
  5. flush():此函式不切換狀態,只是一個同步標識。呼叫後,組建所有非同步操作都給出與同步操作相同的結果。
  6. clear():與flush類似,此函式不切合狀態,是同步標識。呼叫後,組建直接清除所有非同步快取。
  7. resume(): pause=>running狀態切換。
  8. stop():running=>inited 狀態切換。注意如果是pause=>inited 情況,也是stop。值得一提的是,stop之後,不需要將組建快取和條件變數恢復到初始狀態。也就是說,內含flush() 或者 clear()操作。因為stop之後是inited狀態,允許上下快取堆積。
  9. uninitinited=>uninit狀態。個人感覺呼叫習慣是不用相容stop功能。也就是說如果元件是runningpause狀態的話,可以不響應uninit函式。客戶如果想無論什麼情況直接析構,可以組合呼叫stop()和 uninit()
  10. destructor: 析構元件。為了防止記憶體洩露,一般會內建呼叫stop() uninit()

另外,這裡需要對reset做一下說明,有時候客戶希望的reset是恢復到running的初始狀態,但是也有客戶resetuninit 狀態。我個人覺得uninit 可以完成第二種業務。

總結一下如下圖所示:

狀態機轉換示意圖

3.兩種處理狀態方法

處理狀態無非就是看狀態和訊息是否匹配if(state can proess this msg)。有兩種處理方法,第一種是為每個狀態寫一個處理函式,這個處理函式判斷是否響應該訊息:

int processInitstateMsg(const Msg& msg)
{
    switch (msg.type) {
        case start:
            start();
            state = start_state;
            break;
        case uninit:
            ...
        default:
            // 狀態不對,不處理
            break;
    }
}

另一種方法,也可以給每個訊息寫一個響應處理函式(alivc_framework現在的實現)

int processMsg(const Msg& msg)
{
    switch (msg.type) {
            case start:
                return onStartMsg(msg);
            case ...
        default:
            // 未知訊息
            Log.e("unknown msg type.");
    }
}

int onStartMsg(const Msg& startMsg)
{
    if(state ok)
    {
        start();
    }
    else
    {
        return state invalid.
    }
}

對應這兩種設計,誰優誰劣很明顯可以看出。由於元件對於自身狀態型別是明確的,對於外部處理的訊息型別是未知的。如果先按照狀態進行篩選,則只需判斷一次即可(是否這個訊息符合我的狀態),如果按照第二種實現,那就需要判斷兩次了。第一次是processMsg裡面判斷訊息型別(switch msg type) 然後呼叫每個響應訊息的處理函式,然後在每個函式裡面判斷狀態(switch state),進而再進行訊息處理。

當然第二種做法並不是一無是處。它可以提供一個stateBase進行抽象,處理公共的訊息。

4.狀態基類

由於狀態很多,我們可以通過構造一個基類stateBase,來管理狀態。stateBase暴露模組切換函式,內部有三個邏輯組成:1.檢查狀態,2.具體實現,3.切換狀態。以stop為例:

StateBase::Stop()
{
	//1.check state
	if(state != pause && state != running)
	{
		return false;
	}

	// 2.call virtual func impl;
	StopImpl();
	
	// 3.make state
	state = inited;
	return true;
}

這樣派生類就可以直接實現impl 而無需關心狀態了。

不滿足狀態條件的元件如何處理?

實際應用中,有很多不滿足使用基類條件的情況。比如類內部有多個執行緒,並且多個執行緒有相互依賴關係,並不是一個簡單的StartStop能解決的; 又或者某個類是封裝的第三方庫的類,而且這個第三方庫定義的狀態機與我們之前假設的狀態機不同(例如AndroidMediaCodec Flush狀態是不能跳轉到Running態,必須要先ClearInit態等等)。如果遇到這些比較棘手的情況,應該拋棄狀態基類的方法,自己實現狀態機,並且在對外介面中說明狀態切換的時機以及特性。

總結

從類設計角度來講,設計一個狀態機,維護自身狀態來保證呼叫和返回的結果是必要的。

由於每個類具體情況不同,需要每個元件自己實現維護自己的狀態機。

由於音視訊編排元件比較類似,可以提供統一的介面呈現狀態轉換。

編排元件可以抽象成一個基類統一維護,也可以自己處理,各有優劣。


相關文章