關於有限狀態機

一 有限狀態機的實現方式

有限狀態機(Finite State Machine或者Finite State Automata)是軟件領域中一種重要的工具,很多東西的模型實際上就是有限狀態機。
FSM的實現方式:
1) switch/case或者if/else
這無意是最直觀的方式,使用一堆條件判斷,會編程的人都可以做到,對簡單小巧的狀態機來說最合適,但是毫無疑問,這樣的方式比較原始,對龐大的狀態機難以維護。
2) 狀態表
維護一個二維狀態表,橫座標表示當前狀態,縱座標表示輸入,表中一個元素存儲下一個狀態和對應的操作。這一招易於維護,但是運行時間和存儲空間的代價較大。
3) 使用State Pattern
使用State Pattern使得代碼的維護比switch/case方式稍好,性能上也不會有很多的影響,但是也不是100%完美。不過Robert C. Martin做了兩個自動產生FSM代碼的工具,for java和for C++各一個,在http://www.objectmentor.com/resources/index上有免費下載,這個工具的輸入是純文本的狀態機描述,自動產生符合State Pattern的代碼,這樣developer的工作只需要維護狀態機的文本描述,每必要冒引入bug的風險去維護code。
4) 使用宏定義描述狀態機
一般來說,C++編程中應該避免使用#define,但是這主要是因爲如果用宏來定義函數的話,很容易產生這樣那樣的問題,但是巧妙的使用,還是能夠產生奇妙的效果。MFC就是使用宏定義來實現大的架構的。
在實現FSM的時候,可以把一些繁瑣無比的if/else還有花括號的組合放在宏中,這樣,在代碼中可以3)中狀態機描述文本一樣寫,通過編譯器的預編譯處理產生1)一樣的效果,我見過產生C代碼的宏,如果要產生C++代碼,己軟MFC可以,那麼理論上也是可行的。

二 狀態機的兩種寫法+實例

    有限狀態機FSM思想廣泛應用於硬件控制電路設計,也是軟件上常用的一種處理方法(軟件上稱爲FMM--有限消息機)。它把複雜的控制邏輯分解成有限個穩定狀態,在每個狀態上判斷事件,變連續處理爲離散數字處理,符合計算機的工作特點。同時,因爲有限狀態機具有有限個狀態,所以可以在實際的工程上實現。但這並不意味着其只能進行有限次的處理,相反,有限狀態機是閉環系統,有限無窮,可以用有限的狀態,處理無窮的事務。

    有限狀態機的工作原理如圖1所示,發生事件(event)後,根據當前狀態(cur_state),決定執行的動作(action),並設置下一個狀態號(nxt_state)。

                         -------------

                         |           |-------->執行動作action

     發生事件event ----->| cur_state |

                         |           |-------->設置下一狀態號nxt_state

                         -------------

                            當前狀態

                      圖1 有限狀態機工作原理

 

                               e0/a0

                              --->--

                              |    |

                   -------->----------

             e0/a0 |        |   S0   |-----

                   |    -<------------    | e1/a1

                   |    | e2/a2           V

                 ----------           ----------

                 |   S2   |-----<-----|   S1   |

                 ----------   e2/a2   ----------

                       圖2 一個有限狀態機實例

 

              --------------------------------------------

              當前狀態   s0        s1        s2     | 事件

              --------------------------------------------

                       a0/s0      --       a0/s0   |  e0

              --------------------------------------------

                       a1/s1      --        --     |  e1

              --------------------------------------------

                       a2/s2     a2/s2      --     |  e2

              --------------------------------------------

 

               表1 圖2狀態機實例的二維表格表示(動作/下一狀態)

    圖2爲一個狀態機實例的狀態轉移圖,它的含義是:

        在s0狀態,如果發生e0事件,那麼就執行a0動作,並保持狀態不變;

                 如果發生e1事件,那麼就執行a1動作,並將狀態轉移到s1態;

                 如果發生e2事件,那麼就執行a2動作,並將狀態轉移到s2態;

        在s1狀態,如果發生e2事件,那麼就執行a2動作,並將狀態轉移到s2態;

        在s2狀態,如果發生e0事件,那麼就執行a0動作,並將狀態轉移到s0態;

    有限狀態機不僅能夠用狀態轉移圖表示,還可以用二維的表格代表。一般將當前狀態號寫在橫行上,將事件寫在縱列上,如表1所示。其中“--”表示空(不執行動作,也不進行狀態轉移),“an/sn”表示執行動作an,同時將下一狀態設置爲sn。表1和圖2表示的含義是完全相同的。

    觀察表1可知,狀態機可以用兩種方法實現:豎着寫(在狀態中判斷事件)和橫着寫(在事件中判斷狀態)。這兩種實現在本質上是完全等效的,但在實際操作中,效果卻截然不同。

 

==================================

豎着寫(在狀態中判斷事件)C代碼片段

cur_state = nxt_state;

switch(cur_state)

{                  //在當前狀態中判斷事件

        case s0:                        //在s0狀態

            if(e0_event)

{ //如果發生e0事件,那麼就執行a0動作,並保持狀態不變;

                執行a0動作;

              //nxt_state = s0; //因爲狀態號是自身,所以可以刪除此句,以提高運行速度。

            }

            else if(e1_event)

{//如果發生e1事件,那麼就執行a1動作,並將狀態轉移到s1態;

                執行a1動作;

                nxt_state = s1;

            }

            else if(e2_event)

{ //如果發生e2事件,那麼就執行a2動作,並將狀態轉移到s2態;

                執行a2動作;

                nxt_state = s2;

            }

            break;

        case s1:                        //在s1狀態

            if(e2_event)

{    //如果發生e2事件,那麼就執行a2動作,並將狀態轉移到s2態;

                執行a2動作;

                nxt_state = s2;

            }

            break;

        case s2:                        //在s2狀態

            if(e0_event)

{ //如果發生e0事件,那麼就執行a0動作,並將狀態轉移到s0態;

                執行a0動作;

                nxt_state = s0;

            }

    }

 

==================================

橫着寫(在事件中判斷狀態)C代碼片段

==================================

//e0事件發生時,執行的函數

void e0_event_function(int * nxt_state)

{

    int cur_state;

   

    cur_state = *nxt_state;

switch(cur_state)

{

        case s0:                        //觀察表1,在e0事件發生時,s1處爲空

        case s2:

            執行a0動作;

            *nxt_state = s0;

    }

}

 

//e1事件發生時,執行的函數

void e1_event_function(int * nxt_state)

{

    int cur_state;

   

    cur_state = *nxt_state;

switch(cur_state)

{

        case s0:                        //觀察表1,在e1事件發生時,s1和s2處爲空

            執行a1動作;

            *nxt_state = s1;

    }

}

 

//e2事件發生時,執行的函數

void e2_event_function(int * nxt_state)

{

    int cur_state;

   

    cur_state = *nxt_state;

switch(cur_state)

{

        case s0:                        //觀察表1,在e2事件發生時,s2處爲空

        case s1:

            執行a2動作;

            *nxt_state = s2;

    }

}

 

    上面橫豎兩種寫法的代碼片段,實現的功能完全相同,但是,橫着寫的效果明顯好於豎着寫的效果。理由如下:

    1、豎着寫隱含了優先級排序(其實各個事件是同優先級的),排在前面的事件判斷將毫無疑問地優先於排在後面的事件判斷。這種if/else if寫法上的限制將破壞事件間原有的關係。而橫着寫不存在此問題。

    2、由於處在每個狀態時的事件數目不一致,而且事件發生的時間是隨機的,無法預先確定,導致豎着寫淪落爲順序查詢方式,結構上的缺陷使得大量時間被浪費。對於橫着寫,在某個時間點,狀態是唯一確定的,在事件裏查找狀態只要使用switch語句,就能一步定位到相應的狀態,延遲時間可以預先準確估算。而且在事件發生時,調用事件函數,在函數裏查找唯一確定的狀態,並根據其執行動作和狀態轉移的思路清晰簡潔,效率高,富有美感。

    總之,我個人認爲,在軟件裏寫狀態機,使用橫着寫的方法比較妥帖。

下面給出一個計算輸入密碼的兩種狀態機的實現:
1.使用switch/case的狀態機實現

//使用switch/case或者if/else實現的基於狀態機(FSM)的密碼鎖
//只有正確輸入密碼 2479 才能解鎖 
#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>   

typedef enum{   
	STATE0 = 0,   
	STATE1,   
	STATE2,  
	STATE3,   
	STATE4,  
}STATE;
  

int main()   

{   
	char ch; 
	STATE current_state = STATE0;    
	while(1){   
		printf("In put password:");   
		while((ch = getchar()) != '\n')
		{   
			if((ch < '0') || (ch > '9'))
			{   
				printf("Input num,ok?/n");   
				break;   
			}   
			switch(current_state){   
			case STATE0:   
				if(ch == '2')   current_state = STATE1;   
				break;   
			case STATE1:   
				if(ch == '4')   current_state = STATE2;   
				break;   
			case STATE2:   
				if(ch == '7')   current_state = STATE3;   
				break;   
			case STATE3:   
				if(ch == '9')   current_state = STATE4;   
				break;   
			default:   
				current_state = STATE0;   
				break;   
			}   
		}   //end inner while 

		if(current_state == STATE4){   
			printf("Correct, lock is open!\n");   
			current_state =   STATE0;
			
		}else
		{
			printf("Wrong, locked!\n");   
			current_state =   STATE0;
			
		}
		break;
	}   
	return 0;   
} 
2.使用豎排方式寫的狀態機

<p>//使用函數指針實現的基於狀態機(FSM)的密碼鎖
//只有正確輸入密碼 2479 才能解鎖 
#include <stdio.h>  
//這個祕密鎖的密碼是xxxx2479,就是說最後4位是2479,前面若干爲爲0~9裏的數字,也可沒有 
#include <stdlib.h>  
#include <string.h>   </p><p>//定義鎖事件處理函數的函數指針類型
typedef void (*lock_func)(char c);</p><p>typedef enum{   
 
 STATE1 = 0, 
 STATE2,   
 STATE3, 
 STATE4, 
 STATE5,//password pass  
 //...ADD here   
 }STATE;  
STATE state;
//狀態1 
void fp_Press2(char ch)
{
 if (state==STATE1)
 {
  //do sth here 
  state=STATE2;
  printf("Correct, current state is STATE2!"); 
 } 
 else 
 {
  printf("Wrong, current state is not STATTE2!"); 
 }
}</p><p>//狀態2 
void fp_Press4(char ch)
{
 if (state==STATE2)
 {
  printf("Correct, current state is STATE3!"); 
  state=STATE3;  
 } 
 else 
 {
 } 
}</p><p>//狀態3 
void fp_Press7(char ch)
{
 if (state==STATE3)
 {
  printf("Correct, current state is STATE4!"); 
  state=STATE4;  
 } 
 else 
 {
 }
}</p><p>//狀態4 
void  fp_Press9(char ch)
{
 if (state==STATE4)
 {
  printf("Correct, lock is open!"); 
  state=STATE5;  
 } 
 else 
 {
 }
}</p><p>
lock_func g_MatrixArr[5][1] = 
{
    /*輸入2*/
    {fp_Press2},</p><p>    /*輸入4*/
    {fp_Press4},</p><p>    /*輸入7*/
    {fp_Press7},
    
    /*輸入9*/
 {fp_Press9},</p><p> /*輸入其他*/
 {fp_Press2}
};
//結束狀態是NULL
//就是通過 return NULL;表達的結束狀態. 
 
//狀態轉換在這裏 
void lock_handle (void)
{
 char ch;
 while(state!=STATE5)
 {
  ch = getchar();
  switch(ch)
  {
   case '2':
    g_MatrixArr[0][0](ch);
    break;
   case '4':
    g_MatrixArr[1][0](ch);
    break;
   case '7':
    g_MatrixArr[2][0](ch);
    break;
   case '9':
    g_MatrixArr[3][0](ch);
    break;
   default:
    g_MatrixArr[4][0](ch);
    break;  
  }
 }
} </p><p> 
int main()   
{      
 lock_handle();
} 
</p>



發佈了31 篇原創文章 · 獲贊 1 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章