大型与嵌套状态机

时间:2009-09-10 10:36:48

标签: c state-machine

我在一个只有很少(3)个状态的实时系统中有一个状态机。

typedef enum {
    STATE1,
    STATE2,
    STATE3
} state_t;

但是,这些州之间的转换需要相当长的时间并且有自己的细分。所以我有两个选择,要么扩展主状态机,以便表示所有中间状态:

typedef enum {
    STATE1,
    STATE1_PREPARE_TRANSITION_TO_STATE2,
    STATE1_DO_TRANSITION_TO_STATE2,
    STATE1_PREPARE_TRANSITION_TO_STATE3,
    STATE1_DO_TRANSITION_TO_STATE3,
    STATE2,
    ...
} state_t;

或者我为相关的主要状态创建了一个嵌套的状态机:

typedef enum {
    STATE1_NOT_ACTIVE,
    STATE1_NORMAL,
    STATE1_PREPARE_TRANSITION_TO_STATE2,
    STATE1_DO_TRANSITION_TO_STATE2,
    STATE1_PREPARE_TRANSITION_TO_STATE3,
    STATE1_DO_TRANSITION_TO_STATE3
} sub_state1_t;
...

两种可能性都有其优点和缺点。大型的状态机很容易变得混乱和复杂。但是,在第二种情况下使所有状态保持一致并不是微不足道的,许多函数需要有关全局状态和子状态的信息。

我想避免使用必须处理多个并行状态的复杂代码,例如:

if ((global_state == STATE1) &&
    (sub_state_1 == STATE1_DO_TRANSITION_TO_STATE2))
{
    ...
    if (transition_xy_done(...))
    {
        global_state = STATE2;
        sub_state_1 = STATE1_NOT_ACTIVE;
        sub_state_2 = STATE2_NORMAL;
    }
}

这个问题的一般最佳方法是什么:许多小型和嵌套的状态机(有许多无效组合),一个大型状态机或其他任何东西?

6 个答案:

答案 0 :(得分:5)

许多小型状态机将为您提供更多代码灵活性,特别是如果您需要重新设计任何东西。然后,您应该(希望)能够更改嵌套状态机,而无需更改任何其他嵌套状态机。

拥有更大的转换表不应该导致更长的查找,因为我假设您在内存中合理地布置表。如果有的话,你实际上应该能够从大机器中获得更高的速度,因为你没有额外的一两步,小型状态机可以在它们之间干净地过渡。但是考虑到这种方法的复杂性,我建议如下:使用嵌套状态机进行设计,然后一切正常后,如果需要,可以重构为单个状态机以获得一点速度提升。

答案 1 :(得分:4)

首先,我要赞扬你认识到发生了什么并使这些状态显式化(因为它们实际上是模型中的其他状态,而不是真正的动作转换)。我经常看到状态机最终像你最后一个例子(你想避免)。当您在事件处理程序中对“其他”状态变量进行测试时,这表明您的状态机具有更多您已真正投入到设计中的状态 - 这些状态会反映在设计中,而不会卡在现有状态的事件中处理程序与一堆意大利面条编码检查全局变量中编码的额外“状态”。

有几个C ++框架可以模拟分层状态机 - HSM - (这是你的嵌套状态机的想法听起来像),但我唯一知道支持直接C的是Quantum Framework,我认为买入这可能意味着一个体面的承诺(即,这可能不是一个简单的改变)。但是,如果你想研究这种可能性,Samek写了很多关于如何在C中支持HSM的文章(and a book)。

但是,如果你不需要HSM模型的一些更复杂的部分(例如那些未被'内部'状态处理的事件被冒泡,可能由父状态处理,完全输入和退出对整个状态层次结构的支持),然后很容易支持嵌套状态机,就像完全独立的状态机一样,当进入/退出父状态时恰好启动和停止。

大型状态机模型可能更容易实现(在现有框架中只有几个状态)。我建议如果将状态添加到当前状态机模式不会使模型过于复杂,那么就这样做吧。

换句话说,让最适合您的模型的驱动器如何在软件中实现状态机。

答案 2 :(得分:1)

正如您所提到的,大型状态机变得混乱,因此很难维护。几个较小的SM总是更容易理解和维护。

大SM的另一个缺点 - 更大的转换表,因此查找需要更长时间。

答案 3 :(得分:1)

我认为没有单一的,一般的方法。正如其他人所说,这取决于你想要做什么。

从广义上讲,我会避免将较小的状态机嵌套在较大的状态机中,因为当你试图简化事物时,你不仅要添加更多的状态 - 因此也要增加复杂性 - 你现在有两个状态变量可以跟踪

特别是,当在“外部”状态机中遍历状态时,必须正确地初始化“内部”状态变量。例如,如果由于一个错误,外部状态机中的转换无法重置内部状态机的状态变量会怎样呢?

一个可能的例外是所有内部状态机都做同样的事情。如果可以对数据进行参数化(例如,通过使用数组),那么您可以使用内部状态机的单个实现,并且可以使用计数器或类似替换外部状态机。

举一个简单的例子:

#define MyDataSIZE 10

void UpdateStateMachine(void)
{
    static enum {BeginSTATE, DoStuffSTATE, EndSTATE} State = BeginSTATE;
    static unsigned int Counter = 0;
    static unsigned int MyData[MyDataSIZE];

    switch(State)
    {
        default:
        case BeginSTATE:
            /* Some code */
            if(/* Some condition*/)
                {State = DoStuffSTATE;}
            break;
        case DoStuffSTATE:
            /* Some actions on MyData[Counter] */
            if(/* Some condition*/)
                {State = EndSTATE;}
            break;
        case EndSTATE:
            /* Some code */
            if(/* Some condition*/)
            {
                Counter++;
                if(Counter >= MyDataSIZE)
                    {Counter = 0;}
                State = BeginSTATE;
            } /* if */
            break;
    } /* switch */
} /* UpdateStateMachine() */

答案 4 :(得分:0)

为什么不使用state pattern

答案 5 :(得分:0)

我投票给更大的状态机,假设一台机器只能处于一个大型状态机状态,它应该在逻辑上存在。

通过使用一台大型机器,您正在使用环境特性来防止同时存在两种状态的状态,从而使程序更安全,更易读。

另外一个大型状态机的优势在于,任何其他程序员都可以通过查看单个位置(即获得全局)轻松了解所有状态,而不是在单个位置查看,希望能够了解子 - 分区,然后不得不看每个分部。

另外,正如您所建议的那样,使用多个状态机会强制您发送更多参数,为每个状态执行多个测试等等......

至于未来的期望,我相信YAGNI