是否存在典型的状态机实现模式?

我们需要在 C中实现一个简单的状态机。
标准 switch 语句是最好的方法吗?
我们有一个当前状态(state)和一个转换触发器。


switch(state)
{
case STATE_1:
state = DoState1(transition);
break;
case STATE_2:
state = DoState2(transition);
break;
}
...
DoState2(int transition)
{
// Do State Work
...
if(transition == FROM_STATE_2) {
// New state when doing STATE 2 -> STATE 2
}
if(transition == FROM_STATE_1) {
// New State when moving STATE 1 -> STATE 2
}
return new_state;
}

有没有更好的办法

编辑: 对于 C + + ,我认为 Boost 状态图库可能是解决方案。但是,没有对 C 有帮助,让我们集中在 C 用例上。

143400 次浏览

Boost 有状态图库

不过我不知道它的用途,我自己还没有用过

根据我的经验,使用“ switch”语句是处理多种可能状态的标准方法。虽然我很惊讶您正在向每个状态处理传递一个转换值。我认为状态机的全部意义在于每个状态执行一个单独的操作。然后,下一个操作/输入确定要转换到哪个新状态。因此,我希望每个状态处理函数立即执行进入状态所固定的任何操作,然后再决定是否需要转换到另一个状态。

在 C + + 中,考虑 状态模式

还有 逻辑网格,随着状态机变大,它更易于维护

对于简单的情况,可以使用开关样式方法。我发现,在过去行之有效的方法还包括处理转变:

static int current_state;    // should always hold current state -- and probably be an enum or something


void state_leave(int new_state) {
// do processing on what it means to enter the new state
// which might be dependent on the current state
}


void state_enter(int new_state) {
// do processing on what is means to leave the current state
// might be dependent on the new state


current_state = new_state;
}


void state_process() {
// switch statement to handle current state
}
   

我对升级库一无所知,但是这种类型的方法非常简单,不需要任何外部依赖,并且很容易实现。

本文 对于状态模式来说是一篇很好的文章(尽管它是 C + + ,而不是特指 C)。

如果你能把你的手放在书“ 以头为先的设计模式”,解释和例子是非常清楚的。

有一本书叫 C/C + + 中的实用状态图。 然而,它是 方式太重量级,我们需要什么。

我更喜欢对大多数状态机使用表驱动方法:

typedef enum { STATE_INITIAL, STATE_FOO, STATE_BAR, NUM_STATES } state_t;
typedef struct instance_data instance_data_t;
typedef state_t state_func_t( instance_data_t *data );


state_t do_state_initial( instance_data_t *data );
state_t do_state_foo( instance_data_t *data );
state_t do_state_bar( instance_data_t *data );


state_func_t* const state_table[ NUM_STATES ] = {
do_state_initial, do_state_foo, do_state_bar
};


state_t run_state( state_t cur_state, instance_data_t *data ) {
return state_table[ cur_state ]( data );
};


int main( void ) {
state_t cur_state = STATE_INITIAL;
instance_data_t data;


while ( 1 ) {
cur_state = run_state( cur_state, &data );


// do other program logic, run other state machines, etc
}
}

这当然可以扩展到支持多个状态机,等等。转换操作也可以适应:

typedef void transition_func_t( instance_data_t *data );


void do_initial_to_foo( instance_data_t *data );
void do_foo_to_bar( instance_data_t *data );
void do_bar_to_initial( instance_data_t *data );
void do_bar_to_foo( instance_data_t *data );
void do_bar_to_bar( instance_data_t *data );


transition_func_t * const transition_table[ NUM_STATES ][ NUM_STATES ] = {
{ NULL,              do_initial_to_foo, NULL },
{ NULL,              NULL,              do_foo_to_bar },
{ do_bar_to_initial, do_bar_to_foo,     do_bar_to_bar }
};


state_t run_state( state_t cur_state, instance_data_t *data ) {
state_t new_state = state_table[ cur_state ]( data );
transition_func_t *transition =
transition_table[ cur_state ][ new_state ];


if ( transition ) {
transition( data );
}


return new_state;
};

表驱动方法更容易维护和扩展,更容易映射到状态图。

你可能已经看到了我对另一个 C 问题的回答,我提到了 FSM! 下面是我是如何做到的:

FSM {
STATE(x) {
...
NEXTSTATE(y);
}


STATE(y) {
...
if (x == 0)
NEXTSTATE(y);
else
NEXTSTATE(x);
}
}

定义了以下宏

#define FSM
#define STATE(x)      s_##x :
#define NEXTSTATE(x)  goto s_##x

这可以根据具体情况进行修改。例如,你可能有一个文件 FSMFILE,你想驱动你的 FSM,所以你可以合并的行动,读取下一个字符到宏本身:

#define FSM
#define STATE(x)         s_##x : FSMCHR = fgetc(FSMFILE); sn_##x :
#define NEXTSTATE(x)     goto s_##x
#define NEXTSTATE_NR(x)  goto sn_##x

现在有两种类型的转换: 一种转换到一个状态并读取一个新字符,另一种转换到一个状态而不使用任何输入。

你也可以自动处理 EOF,比如:

#define STATE(x)  s_##x  : if ((FSMCHR = fgetc(FSMFILE) == EOF)\
goto sx_endfsm;\
sn_##x :


#define ENDFSM    sx_endfsm:

这种方法的好处是,您可以直接将您绘制的状态图转换为工作代码,相反,您可以轻松地从代码中绘制状态图。

在其他实现有限状态机的技术中,转换的结构隐藏在控制结构中(当然,如果切换的话...) ,并由变量值(通常是 state变量)控制,将漂亮的图表与复杂的代码联系起来可能是一项复杂的任务。

我是从一篇发表在伟大的《计算机语言》杂志上的文章中学到这个技巧的,不幸的是,这篇文章已经不再发表了。

对于一个简单的状态机,只需为您的状态使用 switch 语句和枚举类型。根据输入在 switch 语句中执行转换。在一个实际的程序中,你显然需要改变“ if (input)”来检查你的过渡点。希望这个能帮上忙。

typedef enum
{
STATE_1 = 0,
STATE_2,
STATE_3
} my_state_t;


my_state_t state = STATE_1;


void foo(char input)
{
...
switch(state)
{
case STATE_1:
if(input)
state = STATE_2;
break;
case STATE_2:
if(input)
state = STATE_3;
else
state = STATE_1;
break;
case STATE_3:
...
break;
}
...
}

Switch ()是在 C 中实现状态机的一种强大而标准的方法,但是如果有大量的状态,它会降低可维护性。另一种常见的方法是使用函数指针来存储下一个状态。这个简单的例子实现了一个设置/重置触发器:

/* Implement each state as a function with the same prototype */
void state_one(int set, int reset);
void state_two(int set, int reset);


/* Store a pointer to the next state */
void (*next_state)(int set, int reset) = state_one;


/* Users should call next_state(set, reset). This could
also be wrapped by a real function that validated input
and dealt with output rather than calling the function
pointer directly. */


/* State one transitions to state one if set is true */
void state_one(int set, int reset) {
if(set)
next_state = state_two;
}


/* State two transitions to state one if reset is true */
void state_two(int set, int reset) {
if(reset)
next_state = state_one;
}

你可能想看看 Libero有限状态机发电机软件。从一个状态描述语言和/或一个(窗口)状态图编辑器,你可以为 C,C + + ,Java 和许多其他... 加上漂亮的文档和图表生成代码。 来自 iMatix 的源代码和二进制文件

您的问题类似于“是否存在典型的 Data Base 实现模式”? 答案取决于你想达到什么目标?如果要实现较大的确定性状态机,可以使用模型和状态机生成器。 例子可以在 www.StateSoft.org-SM Gallery. Janusz Dobrowolski 查看

我还使用了表方法。但是,有开销。为什么要存储第二个指针列表?C 语言中没有()的函数是一个常量指针。所以你可以这样做:

struct state;
typedef void (*state_func_t)( struct state* );


typedef struct state
{
state_func_t function;


// other stateful data


} state_t;


void do_state_initial( state_t* );
void do_state_foo( state_t* );
void do_state_bar( state_t* );


void run_state( state_t* i ) {
i->function(i);
};


int main( void ) {
state_t state = { do_state_initial };


while ( 1 ) {
run_state( state );


// do other program logic, run other state machines, etc
}
}

当然,这取决于你的恐惧因素(即安全与速度) ,你可能想检查有效的指针。对于大于三个左右状态的状态机,上面的方法应该比等效的开关或表方法更少的指令。你甚至可以宏观化为:

#define RUN_STATE(state_ptr_) ((state_ptr_)->function(state_ptr_))

另外,我从 OP 的例子中感觉到,在考虑/设计状态机时应该进行简化。我不认为过渡状态应该用于逻辑。每个状态函数都应该能够在没有过去状态外显知识的情况下履行其给定的角色。基本上,您设计的是如何从您所处的状态转换到另一个状态。

最后,不要开始设计基于“功能”边界的状态机,而是使用子函数。相反,根据需要等待某些事情发生之后才能继续的时间来划分状态。这将有助于最小化在得到结果之前必须运行状态机的次数。这在编写 I/O 函数或中断处理程序时非常重要。

此外,还有一些经典 switch 语句的优缺点:

优点:

  • 它是在语言中,所以它是文档化的和清晰的
  • 状态被定义在它们被调用的地方
  • 可以在一个函数调用中执行多个状态
  • 可以在 switch 语句之前和之后执行所有状态的通用代码

缺点:

  • 可以在一个函数调用中执行多个状态
  • 可以在 switch 语句之前和之后执行所有状态的通用代码
  • 开关实现可能比较慢

注意两个同时支持和反对的属性。我认为这种转变使得国家之间有机会进行过多的分享,国家之间的相互依赖可能变得难以管理。然而,对于少数状态,它可能是最具可读性和可维护性的。

我最喜欢的模式之一是状态设计模式。对同一组输入作出不同的反应或表现。
对状态机使用 switch/case 语句的一个问题是,当您创建更多的状态时,switch/case 变得难以阅读/维护,促进了无组织的意大利面代码,并且越来越难以在不破坏某些东西的情况下进行更改。我发现使用设计模式可以帮助我更好地组织数据,这就是抽象的全部意义所在。 与其围绕来自哪个州来设计州代码,不如构造代码,以便在进入新状态时记录该状态。这样,你就可以有效地获得你之前状态的记录。我喜欢@JoshPetit 的回答,他的解决方案更进一步,直接取自 GoF 的书:

StateCtxt.h:

#define STATE (void *)
typedef enum fsmSignal
{
eEnter =0,
eNormal,
eExit
}FsmSignalT;


typedef struct fsm
{
FsmSignalT signal;
// StateT is an enum that you can define any which way you want
StateT currentState;
}FsmT;
extern int STATECTXT_Init(void);
/* optionally allow client context to set the target state */
extern STATECTXT_Set(StateT  stateID);
extern void STATECTXT_Handle(void *pvEvent);

StateCtxt.c:

#include "stateCtxt.h"
#include "statehandlers.h"


typedef STATE (*pfnStateT)(FsmSignalT signal, void *pvEvent);


static FsmT      fsm;
static pfnStateT UsbState ;


int STATECTXT_Init(void)
{
UsbState = State1;
fsm.signal = eEnter;
// use an enum for better maintainability
fsm.currentState = '1';
(*UsbState)( &fsm, pvEvent);
return 0;
}


static void ChangeState( FsmT *pFsm, pfnStateT targetState )
{
// Check to see if the state has changed
if (targetState  != NULL)
{
// Call current state's exit event
pFsm->signal = eExit;
STATE dummyState = (*UsbState)( pFsm, pvEvent);


// Update the State Machine structure
UsbState = targetState ;


// Call the new state's enter event
pFsm->signal = eEnter;
dummyState = (*UsbState)( pFsm, pvEvent);
}
}


void STATECTXT_Handle(void *pvEvent)
{
pfnStateT newState;


if (UsbState != NULL)
{
fsm.signal = eNormal;
newState = (*UsbState)( &fsm, pvEvent );
ChangeState( &fsm, newState );
}
}




void STATECTXT_Set(StateT  stateID)
{
prevState = UsbState;
switch (stateID)
{
case '1':
ChangeState( State1 );
break;
case '2':
ChangeState( State2);
break;
case '3':
ChangeState( State3);
break;
}
}

H:

/* define state handlers */
extern STATE State1(void);
extern STATE State2(void);
extern STATE State3(void);

政府官员 c:

#include "stateCtxt.h:"


/* Define behaviour to given set of inputs */
STATE State1(FsmT *fsm, void *pvEvent)
{
STATE nextState;
/* do some state specific behaviours
* here
*/
/* fsm->currentState currently contains the previous state
* just before it gets updated, so you can implement behaviours
* which depend on previous state here
*/
fsm->currentState = '1';
/* Now, specify the next state
* to transition to, or return null if you're still waiting for
* more stuff to process.
*/
switch (fsm->signal)
{
case eEnter:
nextState = State2;
break;
case eNormal:
nextState = null;
break;
case eExit:
nextState = State2;
break;
}


return nextState;
}


STATE  State3(FsmT *fsm, void *pvEvent)
{
/* do some state specific behaviours
* here
*/
fsm->currentState = '2';
/* Now, specify the next state
* to transition to
*/
return State1;
}


STATE   State2(FsmT *fsm, void *pvEvent)
{
/* do some state specific behaviours
* here
*/
fsm->currentState = '3';
/* Now, specify the next state
* to transition to
*/
return State3;
}

对大多数国家机器而言,尤指。有限的状态机,每个状态将知道它的下一个状态应该是什么,以及转换到下一个状态的条件。对于松散状态设计,情况可能并非如此,因此可以选择公开转换状态的 API。如果需要更多的抽象,可以将每个状态处理程序分离到自己的文件中,这与 GoF 书中的具体状态处理程序相当。如果您的设计很简单,只有几个状态,那么 stateCtxt.c 和 statehandlers.c 都可以合并到一个文件中以简化操作。

对于支持 __COUNTER__的编译器,可以将它们用于简单(但较大)的状态机。

  #define START 0
#define END 1000


int run = 1;
state = START;
while(run)
{
switch (state)
{
case __COUNTER__:
//do something
state++;
break;
case __COUNTER__:
//do something
if (input)
state = END;
else
state++;
break;
.
.
.
case __COUNTER__:
//do something
if (input)
state = START;
else
state++;
break;
case __COUNTER__:
//do something
state++;
break;
case END:
//do something
run = 0;
state = START;
break;
default:
state++;
break;
}
}

使用 __COUNTER__代替硬编码数字的优点是 可以在其他状态的中间添加状态,而不必每次都重新编号。 如果编译器不支持 __COUNTER__,那么在有限的情况下可以谨慎地使用 __LINE__

我在 edx.org 的课程“嵌入式系统——塑造世界 UTAustinX-UT”中发现了一个非常巧妙的 Moore FSM 的 C 实现。6.02 x,第10章,作者 Jonathan Valvano 和 Ramesh Yerraballi..。

struct State {
unsigned long Out;  // 6-bit pattern to output
unsigned long Time; // delay in 10ms units
unsigned long Next[4]; // next state for inputs 0,1,2,3
};


typedef const struct State STyp;


//this example has 4 states, defining constants/symbols using #define
#define goN   0
#define waitN 1
#define goE   2
#define waitE 3




//this is the full FSM logic coded into one large array of output values, delays,
//and next states (indexed by values of the inputs)
STyp FSM[4]={
{0x21,3000,{goN,waitN,goN,waitN}},
{0x22, 500,{goE,goE,goE,goE}},
{0x0C,3000,{goE,goE,waitE,waitE}},
{0x14, 500,{goN,goN,goN,goN}}};
unsigned long currentState;  // index to the current state


//super simple controller follows
int main(void){ volatile unsigned long delay;
//embedded micro-controller configuration omitteed [...]
currentState = goN;
while(1){
LIGHTS = FSM[currentState].Out;  // set outputs lines (from FSM table)
SysTick_Wait10ms(FSM[currentState].Time);
currentState = FSM[currentState].Next[INPUT_SENSORS];
}
}

马丁 · 福勒的 UML 提炼中,他在第10章的状态机关系图(重点是我的)中指出(没有双关语) :

状态图可以通过三种主要方式实现: 嵌套开关状态模式状态表

让我们用一个手机显示状态的简化例子:

enter image description here

嵌套开关

Fowler 给出了一个 C # 代码的例子,但是我已经将它改编成了我的例子。

public void HandleEvent(PhoneEvent anEvent) {
switch (CurrentState) {
case PhoneState.ScreenOff:
switch (anEvent) {
case PhoneEvent.PressButton:
if (powerLow) { // guard condition
DisplayLowPowerMessage(); // action
// CurrentState = PhoneState.ScreenOff;
} else {
CurrentState = PhoneState.ScreenOn;
}
break;
case PhoneEvent.PlugPower:
CurrentState = PhoneState.ScreenCharging;
break;
}
break;
case PhoneState.ScreenOn:
switch (anEvent) {
case PhoneEvent.PressButton:
CurrentState = PhoneState.ScreenOff;
break;
case PhoneEvent.PlugPower:
CurrentState = PhoneState.ScreenCharging;
break;
}
break;
case PhoneState.ScreenCharging:
switch (anEvent) {
case PhoneEvent.UnplugPower:
CurrentState = PhoneState.ScreenOff;
break;
}
break;
}
}

状态模式

下面是我的 GoF State 模式示例的一个实现:

enter image description here

状态表

从 Fowler 那里得到灵感,下面是我的例子:

Source State    Target State    Event         Guard        Action
--------------------------------------------------------------------------------------
ScreenOff       ScreenOff       pressButton   powerLow     displayLowPowerMessage
ScreenOff       ScreenOn        pressButton   !powerLow
ScreenOn        ScreenOff       pressButton
ScreenOff       ScreenCharging  plugPower
ScreenOn        ScreenCharging  plugPower
ScreenCharging  ScreenOff       unplugPower

比较

嵌套开关将所有逻辑保持在一个位置,但是当存在大量状态和转换时,代码很难读取。它可能比其他方法更安全,更容易验证(没有多态性或解释)。

状态模式实现可能会将逻辑分散到几个独立的类中,这可能会使理解整个逻辑成为一个问题。另一方面,小类很容易单独理解。如果通过添加或删除转换来更改行为,则设计特别脆弱,因为它们是层次结构中的方法,并且可能对代码进行大量更改。如果您遵循小型接口的设计原则,那么您将看到这种模式并没有做得很好。但是,如果状态机是稳定的,那么就不需要进行这样的更改。

状态表方法需要为内容编写某种类型的解释器(如果您使用的语言中有反射,这可能会更容易) ,这可能需要大量预先完成的工作。正如 Fowler 指出的那样,如果表与代码是分开的,那么您可以修改软件的行为而不需要重新编译。但是,这有一些安全隐患; 软件的行为是基于外部文件的内容。

编辑(不适用于 C 语言)

还有一种流畅的接口(也称为内部领域特定语言)方法,这可能是由具有 一级函数的语言促进的。无国籍图书馆已经存在,这个 blog 显示了一个带有代码的简单示例。讨论了 Java 实现(预 Java8)。我也看了 GitHub 上的 Python 示例

你可以在 c. abc0中使用极简 UML状态机框架

它支持有限状态机和分层状态机,只有3个 API,2个结构和1个枚举。

状态机由 state_machine_t结构表示。它是一个抽象结构,可以继承它来创建状态机。

//! Abstract state machine structure
struct state_machine_t
{
uint32_t Event;          //!< Pending Event for state machine
const state_t* State;    //!< State of state machine.
};

状态由指向框架中 state_t结构的指针表示。

如果框架配置为有限状态机,则 state_t包含,

typedef struct finite_state_t state_t;


// finite state structure
typedef struct finite_state_t{
state_handler Handler;        //!< State handler function (function pointer)
state_handler Entry;          //!< Entry action for state (function pointer)
state_handler Exit;           //!< Exit action for state (function pointer)
}finite_state_t;

该框架提供了一个 API dispatch_event将事件分派到状态机,以及两个用于状态遍历的 API。

state_machine_result_t dispatch_event(state_machine_t* const pState_Machine[], uint32_t quantity);
state_machine_result_t switch_state(state_machine_t* const, const state_t*);


state_machine_result_t traverse_state(state_machine_t* const, const state_t*);

有关如何实现分层状态机的详细信息,请参考 GitHub 存储库。

代码示例
Https://github.com/kiishor/uml-state-machine-in-c/blob/master/demo/simple_state_machine/readme.md
Https://github.com/kiishor/uml-state-machine-in-c/blob/master/demo/simple_state_machine_enhanced/readme.md

我还希望采用表驱动的方法。我过去使用过 switch语句。我遇到的主要问题是调试转换并确保设计的状态机得到正确实现。这发生在存在大量状态和事件的情况下。

使用表驱动的方法将状态和转换总结在一个地方。

下面是这种方法的演示。

/*Demo implementations of State Machines
*
* This demo leverages a table driven approach and function pointers
*
* Example state machine to be implemented
*
*          +-----+      Event1        +-----+      Event2        +-----+
*    O---->|  A  +------------------->|  B  +------------------->|  C  |
*          +-----+                    +-----+                    +-----+
*             ^                                                     |
*             |                       Event3                        |
*             +-----------------------------------------------------+
*
* States: A, B, C
* Events: NoEvent (not shown, holding current state), Event1, Event2, Event3
*
* Partly leveraged the example here: http://web.archive.org/web/20160808120758/http://www.gedan.net/2009/03/18/finite-state-machine-matrix-style-c-implementation-function-pointers-addon/
*
* This sample code can be compiled and run using GCC.
* >> gcc -o demo_state_machine demo_state_machine.c
* >> ./demo_state_machine
*/


#include <stdio.h>
#include <assert.h>


// Definitions of state id's, event id's, and function pointer
#define N_STATES  3
#define N_EVENTS  4


typedef enum {
STATE_A,
STATE_B,
STATE_C,
} StateId;


typedef enum {
NOEVENT,
EVENT1,
EVENT2,
EVENT3,
} Event;
typedef void (*StateRoutine)();


// Assert on number of states and events defined
static_assert(STATE_C==N_STATES-1,
"Number of states does not match defined number of states");
static_assert(EVENT3==N_EVENTS-1,
"Number of events does not match defined number of events");


// Defining State, holds both state id and state routine
typedef struct {
StateId id;
StateRoutine routine;
}  State;


// General functions
void evaluate_state(Event e);


// State routines to be executed at each state
void state_routine_a(void);
void state_routine_b(void);
void state_routine_c(void);


// Defining each state with associated state routine
const State state_a = {STATE_A, state_routine_a};
const State state_b = {STATE_B, state_routine_b};
const State state_c = {STATE_C, state_routine_c};


// Defning state transition matrix as visualized in the header (events not
// defined, result in mainting the same state)
State state_transition_mat[N_STATES][N_EVENTS] = {
{ state_a, state_b, state_a, state_a},
{ state_b, state_b, state_c, state_b},
{ state_c, state_c, state_c, state_a}};


// Define current state and initialize
State current_state = state_a;


int main()
{
while(1) {
// Event to receive from user
int ev;


printf("----------------\n");
printf("Current state: %c\n", current_state.id + 65);
printf("Event to occur: ");
// Receive event from user
scanf("%u", &ev);
evaluate_state((Event) ev); // typecast to event enumeration type
printf("-----------------\n");
};
return (0);
}


/*
* Determine state based on event and perform state routine
*/
void evaluate_state(Event ev)
{
//Determine state based on event
current_state = state_transition_mat[current_state.id][ev];
printf("Transitioned to state: %c\n", current_state.id + 65);
// Run state routine
(*current_state.routine)();
}


/*
* State routines
*/
void state_routine_a() {
printf("State A routine ran. \n");


}
void state_routine_b() {
printf("State B routine ran. \n");
}
void state_routine_c() {
printf("State C routine ran. \n");
}