关于有限状态机

一 有限状态机的实现方式

有限状态机(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>



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值