轉自:https://blog.csdn.net/wuhenyouyuyouyu/article/details/53407936html
出處:http://xlambda.com/blog/2014/11/04/hierarchical-state-machine/git
計算機程序是寫給人看的,只是順便能運行。程序員
—— 《計算機程序的構造和解釋》 [1]github
在計算機領域,FSM(有限狀態機)是一個在自動機理論和程序設計實踐中很常見的術語,簡單來講,有限狀態機表示的是系統根不一樣輸入/不一樣條件在各個狀態之間進行跳轉的模型。能夠經過圖或表來描述有限狀態機,這種圖或表通常被稱爲狀態圖/狀態轉移圖(State Chart)或狀態轉移表。由於圖更加直觀,本文統一使用狀態圖來描述有限狀態機。編程
在狀態圖裏面,通常用圓圈或方框來表示狀態,用箭頭來表示狀態之間的跳轉,箭頭能夠帶上跳轉須要的輸入或條件,也能夠帶附帶其它描述。一個從空白處引出,沒有源狀態的箭頭則表示整個系統的啓動,啓動後進入的第一個狀態能夠稱爲開始狀態,能夠用雙重圓圈特別標出。整個狀態圖就是一個有圓圈,箭頭及描述的有向圖形。下面是一個 簡單例子 。設計模式
上圖表示一個接受二進制輸入(輸入爲0或者1),計算輸入包含奇數仍是偶數個0的狀態機。其中S1狀態表示」偶數個0」,S2表示」奇數個0」。系統啓動後,沿着圖中最左邊的箭頭進入S1狀態,此時沒有讀入任何輸入(0個0)。S1圓圈上方帶1的箭頭表示若是輸入是1,則跳轉到S1,即保持原狀態不變。若是輸入是0,則跳轉到S2。其它箭頭也能夠相似理解。當所有輸入都處理完以後,只需看當前狀態是S1仍是S2便可得出結論:輸入具備奇數個0仍是偶數個0。網絡
因爲狀態機能夠將問題總體的分解成各個部分狀態及跳轉,直觀地對系統進行建模,因此它不只被用於理論研究過程中,並且被普遍用於程序設計實踐,在操做系統,網絡協議棧,各類分佈式應用中均可以見到它的身影。app
由上面的表述咱們得知,FSM是對系統的建模,是將問題/解決方案以一種條理化系統化的方式表達出來,映射到人的認知層面,而要在程序中表達FSM,也須要必定的建模工具,即用某種代碼編寫的方式(或稱之爲FSM模式),將FSM以一種條理化系統化的方式映射到代碼層面。在程序設計領域,到底有哪些經常使用的FSM實現方式呢?下面咱們來作一個簡單的回顧。echarts
從簡單到複雜,下面咱們瀏覽一下常見的幾種FSM實現模式[2]。框架
自從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個,整個代碼像一鍋粥同樣粘糊在一塊兒,變得難於理解和維護。
另外一個比較流行的模式是狀態表模式。狀態表模式是指將全部的狀態和跳轉邏輯規劃成一個表格來表達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,在常見的場景中,這張狀態表可能十分大,佔用大量的內存空間,然而中間包含的有效狀態跳轉項卻相對少,也就是說狀態表是一個稀疏的表。
在OOP的 設計模式 [6]中,有一個狀態模式能夠用於表達狀態機。狀態模式基於OOP中的代理和多態。父類定義一系列通用的接口來處理輸入事件,作爲狀態機的對外接口形態。每一個包含具體邏輯的子類各表示狀態機裏面的一個狀態,實現父類定義好的事件處理接口。而後定義一個指向具體子類對象的變量標記當前的狀態,在一個上下文相關的環境中執行此變量對應的事件處理方法,來表達狀態機。依然使用上述例子,用狀態模式編寫出的代碼以下:
|
狀態模式將各個狀態的邏輯局部化到每一個狀態類,事件分發和狀態跳轉的性能也很高,內存使用上也至關高效,沒有稀疏表浪費內存的問題。它將狀態和事件經過接口繼承分隔開,實現的時候不須要列舉全部事件,添加狀態也只是添加子類實現,但要求有一個context類來管理上下文及全部相關的變量,狀態類與context類之間的訪問多了一個間接層,在某些語言裏面可能會遇到封裝問題(好比在C++裏面訪問private字段要使用friend關鍵字)。
結合上述幾種FSM實現模式,咱們能夠獲得一個優化的FSM實現模式,它用對象方法表示狀態,將狀態表嵌套到每一個狀態方法中,所以它包含了上述幾種模式的優勢:事件和狀態的分離,高效的狀態跳轉和內存使用,直接的變量訪問,直觀並且擴展方便。用它重寫上述例子,獲得下述的代碼:
|
在這種模式中能夠添加整個狀態機的初始化動做,每一個狀態的進入/退出動做。上述代碼中 ZeroCounter.S1()
方法的 case EventInitialize
分支能夠放入狀態機的初始化邏輯,每一個狀態方法的 case EventStateEntry
和 case EventStateExit
分支能夠放入對應狀態的進入/退出動做。這是一個重要的特性,在實際狀態機編程中每一個狀態能夠定製進入/退出動做是頗有用的。
上述幾種模式中,狀態之間都是相互獨立的,狀態圖沒有重合的部分,整個狀態機都是平坦的。然而實際上對不少問題的狀態機模型都不會是那麼簡單,有可能問題域自己就有狀態嵌套的概念,有時爲了重用大段的處理邏輯或代碼,咱們也須要支持嵌套的狀態。這方面一個經典的例子就是圖形應用程序的編寫,經過圖形應用程序的框架(如MFC, GTK, Qt)編寫應用程序,程序員只須要註冊少數感興趣的事件響應,如點擊某個按鈕,大部分其它的事件響應都由默認框架處理,如程序的關閉。用狀態機來建模,框架就是父狀態,而應用程序就是子狀態,子狀態只須要處理它感興趣的少數事件,大部分事件都由向上傳遞到框架這個父狀態來處理,這兩種系統之間有一個直觀的類比關係,以下圖所示:
這種事件向父層傳遞,子層繼承了父類行爲的結構,咱們將其稱爲 行爲繼承 ,以區別開OOP裏面的 類繼承 。並把這種包含嵌套狀態的狀態機稱爲 HSM(hierarchical state machine) ,層次狀態機。
加上了對嵌套狀態的支持以後,狀態機的模型就能夠變得任意複雜了,大大的擴大了狀態機的適用場景和範圍,如此一來用狀態機對問題建模就比如用OOP對系統進行編程:識別出系統的狀態及子狀態,並將邏輯固化到狀態及它們的跳轉邏輯當中。
那麼在狀態機實現模式裏如何支持嵌套狀態呢?從整個狀態圖來看,狀態/子狀態所造成的這張大圖本質上是一個單根的樹結構,每一個狀態圖有一個根結點top,每一個狀態是一個樹結點,能夠帶任意多的子狀態/子結點,每一個子狀態只有一個父結點,要表達嵌套狀態,就是要構造出這樣一棵樹。
我用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》的出版,其中文譯本見 這裏 。