轉自:https://blog.csdn.net/wuhenyouyuyouyu/article/details/53407936
出處:http://xlambda.com/blog/2014/11/04/hierarchical-state-machine/
計算機程序是寫給人看的,只是順便能運行。
—— 《計算機程序的構造和解釋》 [1]
FSM
在計算機領域,FSM(有限狀態機)是一個在自動機理論和程序設計實踐中很常見的術語,簡單來說,有限狀態機表示的是系統根不同輸入/不同條件在各個狀態之間進行跳轉的模型。可以通過圖或表來描述有限狀態機,這種圖或表一般被稱為狀態圖/狀態轉移圖(State Chart)或狀態轉移表。因為圖更加直觀,本文統一使用狀態圖來描述有限狀態機。
在狀態圖里面,一般用圓圈或方框來表示狀態,用箭頭來表示狀態之間的跳轉,箭頭可以帶上跳轉需要的輸入或條件,也可以帶附帶其它描述。一個從空白處引出,沒有源狀態的箭頭則表示整個系統的啟動,啟動后進入的第一個狀態可以稱為開始狀態,可以用雙重圓圈特別標出。整個狀態圖就是一個有圓圈,箭頭及描述的有向圖形。下面是一個 簡單例子 。
上圖表示一個接受二進制輸入(輸入為0或者1),計算輸入包含奇數還是偶數個0的狀態機。其中S1狀態表示”偶數個0”,S2表示”奇數個0”。系統啟動后,沿着圖中最左邊的箭頭進入S1狀態,此時沒有讀入任何輸入(0個0)。S1圓圈上方帶1的箭頭表示如果輸入是1,則跳轉到S1,即保持原狀態不變。如果輸入是0,則跳轉到S2。其它箭頭也可以類似理解。當全部輸入都處理完之后,只需看當前狀態是S1還是S2即可得出結論:輸入具有奇數個0還是偶數個0。
由於狀態機可以將問題整體的分解成各個部分狀態及跳轉,直觀地對系統進行建模,所以它不僅被用於理論研究過程當中,而且被廣泛用於程序設計實踐,在操作系統,網絡協議棧,各種分布式應用中都可以見到它的身影。
程序設計中的FSM
由上面的表述我們得知,FSM是對系統的建模,是將問題/解決方案以一種條理化系統化的方式表達出來,映射到人的認知層面,而要在程序中表達FSM,也需要一定的建模工具,即用某種代碼編寫的方式(或稱之為FSM模式),將FSM以一種條理化系統化的方式映射到代碼層面。在程序設計領域,到底有哪些常用的FSM實現方式呢?下面我們來做一個簡單的回顧。
從簡單到復雜,下面我們瀏覽一下常見的幾種FSM實現模式[2]。
a. 嵌套if-else/switch模式
自從1960年 第一個Lisp實現 引入條件表達式以來, if-else/switch語句 [3]已經成為每個程序員手頭必備的工具,每當需要”根據不同條件進入不同分支”,就搬出它來組織代碼,這與FSM里面”狀態之間根據不同輸入進行跳轉”的概念有簡單的對應關系,這就使得if-else/switch語句成為人們要表達FSM時最先選擇的方式。
仍然以圖1例子進行說明,我們用if-else/switch語句編寫它的實現代碼,用一個變量state表示當前狀態,state可以取兩個值S1, S2,輸入input表示下一個輸入的數字是0還是1,那么就有下列代碼[4]:
|
上面的代碼有一個明顯的嵌套形式的結構,最外層的 switch
語句是根據當前狀態state變量進入不同的分支,內層 switch
針對的則是輸入,所有代碼像掛在衣櫃中的衣服一樣從上到下一一陳列,結構比較清晰。這種嵌套形式if-else/switch語句的FSM代碼組織方式,我們將其稱之為 嵌套if-else/switch 模式。由於這種模式實現起來比較直觀簡明,所以它最為常見。
嵌套if-else/switch具有形式嵌套,代碼集中化的特點,它只適合用來表達狀態個數少,或者狀態間跳轉邏輯比較簡單的FSM。嵌套意味着縮進層次的疊加,一個像圖1那么簡單的實現就需要縮進4層,如果狀態間的邏輯變得復雜,所需要的縮進不斷疊加,代碼在水平方向上會發生膨脹;集中化意味着如果狀態個數增多,輸入變復雜,代碼從垂直方向上會發生指數級別的膨脹。即使通過簡化空分支,抽取邏輯到命名函數[5]等方法來”壓縮”水平/垂直方向上的代碼行數,依然無法從根本上解決膨脹問題,代碼膨脹后造成可讀性和可寫性的急劇下降,例如某個狀態里面負責正確設置20個相關變量,而下翻了幾屏代碼之后,下面的某個狀態又用到上面20個變量里面其中的5個,整個代碼像一鍋粥一樣粘糊在一起,變得難於理解和維護。
a. 狀態表
另一個比較流行的模式是狀態表模式。狀態表模式是指將所有的狀態和跳轉邏輯規划成一個表格來表達FSM。仍然以圖1為例子,系統中有兩個狀態S1和S2,不算自跳轉,S1和S2之間只有兩個跳轉,我們用不同行來表示不同的狀態,用不同的列來表示不同的輸入,那么整個狀態圖可以組織成一張表格:
State\Input | Zero | One |
---|---|---|
S1 | DoSomething, S2 | null |
S2 | DoSomething, S1 | null |
對應S1行, Zero列的”DoSomething, S2”表示當處於狀態S1時,如果遇到輸入為Zero,那么就執行動作DoSomething,然后跳轉到狀態S2。由於圖1的例子狀態圖非常簡單,DoSomething動作為空,這里將它特別的列出來只是為了說明在更一般化的情況下如果有其它邏輯可以放到這里來。根據這個狀態表,我們可以寫出下列代碼:
|
從上述例子我們可以看到,用這種方式實現出來的代碼跟畫出來的狀態表有一個直觀的映射關系,它要求程序員將狀態的划分和跳轉邏輯細分到一定的合適大小的粒度,事件驅動的過程查找是對狀態表的直接下標索引,性能也很高。狀態表的大小是不同狀態數量S和不同輸入數量I的一個乘積 S * I,在常見的場景中,這張狀態表可能十分大,占用大量的內存空間,然而中間包含的有效狀態跳轉項卻相對少,也就是說狀態表是一個稀疏的表。
c. 狀態模式
在OOP的 設計模式 [6]中,有一個狀態模式可以用於表達狀態機。狀態模式基於OOP中的代理和多態。父類定義一系列通用的接口來處理輸入事件,做為狀態機的對外接口形態。每個包含具體邏輯的子類各表示狀態機里面的一個狀態,實現父類定義好的事件處理接口。然后定義一個指向具體子類對象的變量標記當前的狀態,在一個上下文相關的環境中執行此變量對應的事件處理方法,來表達狀態機。依然使用上述例子,用狀態模式編寫出的代碼如下:
|
狀態模式將各個狀態的邏輯局部化到每個狀態類,事件分發和狀態跳轉的性能也很高,內存使用上也相當高效,沒有稀疏表浪費內存的問題。它將狀態和事件通過接口繼承分隔開,實現的時候不需要列舉所有事件,添加狀態也只是添加子類實現,但要求有一個context類來管理上下文及所有相關的變量,狀態類與context類之間的訪問多了一個間接層,在某些語言里面可能會遇到封裝問題(比如在C++里面訪問private字段要使用friend關鍵字)。
5. 優化的FSM實現
結合上述幾種FSM實現模式,我們可以得到一個優化的FSM實現模式,它用對象方法表示狀態,將狀態表嵌套到每個狀態方法中,因此它包含了上述幾種模式的優點:事件和狀態的分離,高效的狀態跳轉和內存使用,直接的變量訪問,直觀而且擴展方便。用它重寫上述例子,得到下述的代碼:
|
在這種模式中可以添加整個狀態機的初始化動作,每個狀態的進入/退出動作。上述代碼中 ZeroCounter.S1()
方法的 case EventInitialize
分支可以放入狀態機的初始化邏輯,每個狀態方法的 case EventStateEntry
和 case EventStateExit
分支可以放入對應狀態的進入/退出動作。這是一個重要的特性,在實際狀態機編程中每個狀態可以定制進入/退出動作是很有用的。
6. HSM
上述幾種模式中,狀態之間都是相互獨立的,狀態圖沒有重合的部分,整個狀態機都是平坦的。然而實際上對很多問題的狀態機模型都不會是那么簡單,有可能問題域本身就有狀態嵌套的概念,有時為了重用大段的處理邏輯或代碼,我們也需要支持嵌套的狀態。這方面一個經典的例子就是圖形應用程序的編寫,通過圖形應用程序的框架(如MFC, GTK, Qt)編寫應用程序,程序員只需要注冊少數感興趣的事件響應,如點擊某個按鈕,大部分其它的事件響應都由默認框架處理,如程序的關閉。用狀態機來建模,框架就是父狀態,而應用程序就是子狀態,子狀態只需要處理它感興趣的少數事件,大部分事件都由向上傳遞到框架這個父狀態來處理,這兩種系統之間有一個直觀的類比關系,如下圖所示:
這種事件向父層傳遞,子層繼承了父類行為的結構,我們將其稱為 行為繼承 ,以區別開OOP里面的 類繼承 。並把這種包含嵌套狀態的狀態機稱為 HSM(hierarchical state machine) ,層次狀態機。
加上了對嵌套狀態的支持之后,狀態機的模型就可以變得任意復雜了,大大的擴大了狀態機的適用場景和范圍,如此一來用狀態機對問題建模就好比用OOP對系統進行編程:識別出系統的狀態及子狀態,並將邏輯固化到狀態及它們的跳轉邏輯當中。
那么在狀態機實現模式里如何支持嵌套狀態呢?從整個狀態圖來看,狀態/子狀態所形成的這張大圖本質上是一個單根的樹結構,每個狀態圖有一個根結點top,每個狀態是一個樹結點,可以帶任意多的子狀態/子結點,每個子狀態只有一個父結點,要表達嵌套狀態,就是要構造出這樣一棵樹。
go-hsm
我用Golang編寫了一個HSM框架 go-hsm ,設計要點如下:
- 用類來表示狀態,局部化狀態內部變量及邏輯,整棵狀態樹由具體應用構造
- 支持嵌套狀態及行為繼承,支持進入退出動作,
- 支持自定義事件,支持靜態和動態兩種跳轉
它的代碼在 這里 ,go-hsm的使用例子則放在另一個項目 go-hsm-examples 。由於Golang本身的語言特點,有一些地方的實現較其它語言多了一些缺點,比如Golang里面的binding是靜態的,為了將子類對象的指針傳播到父類方法,要顯式傳遞self指針,子類的接口函數也需要由應用重寫。但由於HSM本身的靈活強大, go-hsm
具有良好的可調整性及擴展性,是一個很好的狀態機建模工具,一門靈活有效表達復雜狀態機的 DSL(Domain Specific Language) 。
注:
[1] 《Structure and Interpretation of Computer Programs》 一書的中文譯本。
[2] 本文內容主要出自Miro Samek博士的經典著作《Practical Statecharts in C/C++: Quantum Programmming for Embedded Systems》,其中文譯本書名為《嵌入式系統的微模塊化程序設計:實用狀態圖C/C++實現》,翻譯甚差,不推薦閱讀。
[3] 各種編程語言里面的條件判斷關鍵字和語句都不盡相同,有的if語句帶 then
關鍵字,有的不帶 then
,有的支持 switch
,這里將它們簡單統稱為if-else/switch語句。
[4] 本文中所有代碼都為Go語言代碼。
[5] 之所以強調函數是命名的,是因為很多語言支持匿名函數(即lambda函數),在嵌套if-else/switch模式內部寫匿名函數定義對降低代碼膨脹起不了作用。
[6] OOP領域設計模式的流行,源於這本書《Design Patterns: Elements of Reusable Object-Oriented Software》的出版,其中文譯本見 這里 。