用于状态机

时间:2008-11-01 17:27:00

标签: state-machine

在哪些编程领域我会使用状态机?为什么?我怎么能实现一个?

编辑:请提供一个实际的例子,如果问的话不是太多。

18 个答案:

答案 0 :(得分:79)

在哪些编程领域我会使用状态机?

使用状态机来表示可以在有限条件(“状态”)中存在的(实际或逻辑)对象,并根据固定集从一个状态前进到下一个状态规则。

为什么我要使用状态机?

状态机通常是非常紧凑的方式来表示一组复杂的规则和条件,并处理各种输入。您将在嵌入式设备中看到内存有限的状态机。实现得很好,状态机是自我记录的,因为每个逻辑状态代表一个物理条件。与其程序等价物相比,状态机可以用微小的代码体现,并且运行效率极高。此外,管理状态变化的规则通常可以作为数据存储在表中,从而提供易于维护的紧凑表示。

如何实施?

琐碎的例子:

enum states {      // Define the states in the state machine.
  NO_PIZZA,        // Exit state machine.
  COUNT_PEOPLE,    // Ask user for # of people.
  COUNT_SLICES,    // Ask user for # slices.
  SERVE_PIZZA,     // Validate and serve.
  EAT_PIZZA        // Task is complete.
} STATE;

STATE state = COUNT_PEOPLE;
int nPeople, nSlices, nSlicesPerPerson;

// Serve slices of pizza to people, so that each person gets
/// the same number of slices.   
while (state != NO_PIZZA)  {
   switch (state)  {
   case COUNT_PEOPLE:  
       if (promptForPeople(&nPeople))  // If input is valid..
           state = COUNT_SLICES;       // .. go to next state..
       break;                          // .. else remain in this state.
   case COUNT_SLICES:  
       if (promptForSlices(&nSlices))
          state = SERVE_PIZZA;
        break;
   case SERVE_PIZZA:
       if (nSlices % nPeople != 0)    // Can't divide the pizza evenly.
       {                             
           getMorePizzaOrFriends();   // Do something about it.
           state = COUNT_PEOPLE;      // Start over.
       }
       else
       {
           nSlicesPerPerson = nSlices/nPeople;
           state = EAT_PIZZA;
       }
       break;
   case EAT_PIZZA:
       // etc...
       state = NO_PIZZA;  // Exit the state machine.
       break;
   } // switch
} // while

备注:

  • 为简单起见,该示例使用带有显式switch() / case状态的break。在实践中,case通常会“落到”下一个州。

  • 为了便于维护大型状态机,可以将每个case中完成的工作封装在“工作”功能中。获取while()顶部的任何输入,将其传递给worker函数,并检查worker的返回值以计算下一个状态。

  • 对于紧凑性,整个switch()可以用函数指针数组替换。每个状态由一个函数体现,其返回值是指向下一个状态的指针。 警告:这可以简化状态机或使其完全无法维护,因此请仔细考虑实施!

  • 嵌入式设备可以实现为仅在发生灾难性错误时退出的状态机,然后执行硬重置并重新进入状态机。

答案 1 :(得分:21)

已经有了一些很好的答案。对于稍微不同的视角,请考虑在较大的字符串中搜索文本。有人已经提到过正则表达式,这实际上只是一个特例,虽然很重要。

考虑以下方法调用:

very_long_text = "Bereshit bara Elohim et hashamayim ve'et ha'arets." …
word = "Elohim"
position = find_in_string(very_long_text, word)

您将如何实施find_in_string?简单的方法将使用嵌套循环,如下所示:

for i in 0 … length(very_long_text) - length(word):
    found = true
    for j in 0 … length(word):
        if (very_long_text[i] != word[j]):
            found = false
            break
    if found: return i
return -1

除了效率低下之外,它还会形成一个状态机!这里的州有点隐藏;让我稍微重写代码,使它们更加明显:

state = 0
for i in 0 … length(very_long_text) - length(word):
    if very_long_text[i] == word[state]:
        state += 1
        if state == length(word) + 1: return i
    else:
        state = 0
return -1

这里的不同状态直接代表我们搜索的单词中的所有不同位置。图中的每个节点都有两个转换:如果字母匹配,则转到下一个状态;对于每个其他输入(即当前位置的每隔一个字母),返回零。

这种轻微的重新制定具有巨大的优势:现在可以使用一些基本技术调整它以产生更好的性能。实际上,每个高级字符串搜索算法(暂时折扣索引数据结构)都建立在这个状态机之上,并改进了它的某些方面。

答案 2 :(得分:13)

什么样的任务?

任何任务,但从我所看到的,任何类型的解析经常被实现为状态机。

<强>为什么吗

解析语法通常不是一项简单的任务。在设计阶段,绘制状态图以测试解析算法是相当普遍的。将其转换为状态机实现是一项相当简单的任务。

如何吗

嗯,你只受想象力的限制。

我已经看到它与case statements and loops完成了。

我已经看到它使用labels and goto语句

完成了

我甚至看到它用表示当前状态的函数指针结构完成。当状态发生变化时,会更新一个或多个function pointer

我已经看到它仅在代码中完成,其中状态的更改仅意味着您在不同的代码段中运行。 (没有状态变量,必要时还有redundent代码。这可以作为一种非常简单的排序来演示,这对于非常小的数据集非常有用。

int a[10] = {some unsorted integers};

not_sorted_state:;
    z = -1;
    while (z < (sizeof(a) / sizeof(a[0]) - 1)
    {
        z = z + 1
        if (a[z] > a[z + 1])
        {
            // ASSERT The array is not in order
            swap(a[z], a[z + 1];        // make the array more sorted
            goto not_sorted_state;      // change state to sort the array
        }
    }
    // ASSERT the array is in order

没有状态变量,但代码本身代表状态

答案 3 :(得分:8)

State设计模式是一种面向对象的方式,通过有限状态机来表示对象的状态。它通常有助于降低该对象实现的逻辑复杂性(嵌套if,许多标志等)

答案 4 :(得分:6)

大多数工作流程都可以作为状态机实现。例如,处理离开申请或订单。

如果您使用的是.NET,请尝试使用Windows Workflow Foundation。您可以使用它快速实现状态机工作流程。

答案 5 :(得分:4)

如果您正在使用C#,那么每次编写迭代器块时,都要求编译器为您构建状态机(跟踪迭代器中的位置等)。

答案 6 :(得分:4)

这是状态机的测试和工作示例。假设您在串行流上(串行端口,tcp / ip数据或文件是典型示例)。在这种情况下,我正在寻找一个特定的数据包结构,可以分为三个部分,同步,长度和有效负载。我有三个状态,一个是空闲,等待同步,第二个是我们有一个很好的同步,下一个字节应该是长度,第三个状态是累积有效载荷。

该示例纯粹是串行的,只有一个缓冲区,如此处写的它将从坏的字节或数据包中恢复,可能丢弃数据包但最终恢复,您可以执行其他操作,如滑动窗口以允许立即恢复。这就是你所说的部分数据包被缩短然后新的完整数据包开始的地方,下面的代码不会检测到这个并且将丢弃部分和整个数据包并在下一个数据包中恢复。如果您确实需要处理所有整个数据包,那么滑动窗口可以为您节省时间。

我一直使用这种状态机是串行数据流,tcp / ip,文件i / o。或者也许tcp / ip协议本身,说你要发送电子邮件,打开端口,等待服务器发送响应,发送HELO,等待服务器发送数据包,发送数据包,等待回复,基本上在这种情况下以及在下面的情况下,您可能正在等待下一个字节/数据包进入。要记住您在等待的内容,还要重新使用等待您可以使用的内容的代码状态变量。与状态机在逻辑中使用的方式相同(等待下一个时钟,我在等什么)。

就像在逻辑中一样,你可能想为每个状态做一些不同的事情,在这种情况下如果我有一个好的同步模式我将偏移量重置到我的存储器中以及重置校验和累加器。数据包长度状态表示您可能希望中止正常控制路径的情况。并非所有,事实上许多状态机可能会跳转或者可能在正常路径中循环,下面的状态机几乎是线性的。

我希望这很有用,并希望在软件中更多地使用状态机。

测试数据存在状态机恢复的故意问题。在第一个良好的数据包之后存在一些垃圾数据,具有错误校验和的数据包和具有无效长度的数据包。我的输出是:

好包:FA0712345678EB 无效的同步模式0x12 无效的同步模式0x34 无效的同步模式0x56 校验和错误0xBF 数据包长度无效0 无效的同步模式0x12 无效的同步模式0x34 无效的同步模式0x56 无效的同步模式0x78 无效的同步模式0xEB 好包:FA081234567800EA 没有更多的测试数据

尽管数据不好,但仍然提取了流中的两个好数据包。检测到并处理了不良数据。

 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>

unsigned char testdata[] =
{
    0xFA,0x07,0x12,0x34,0x56,0x78,0xEB,  
    0x12,0x34,0x56,  
    0xFA,0x07,0x12,0x34,0x56,0x78,0xAA,  
    0xFA,0x00,0x12,0x34,0x56,0x78,0xEB,  
    0xFA,0x08,0x12,0x34,0x56,0x78,0x00,0xEA  
};

unsigned int testoff=0;

//packet structure  
// [0] packet header 0xFA  
// [1] bytes in packet (n)  
// [2] payload  
// ... payload  
// [n-1] checksum  
//  

unsigned int state;

unsigned int packlen;  
unsigned int packoff;  
unsigned char packet[256];  
unsigned int checksum;  

int process_packet( unsigned char *data, unsigned int len )  
{  
    unsigned int ra;  

    printf("good packet:");
    for(ra=0;ra<len;ra++) printf("%02X",data[ra]);
    printf("\n");
}  
int getbyte ( unsigned char *d )  
{  
    //check peripheral for a new byte  
    //or serialize a packet or file  

    if(testoff<sizeof(testdata))
    {
        *d=testdata[testoff++];
        return(1);
    }
    else
    {
        printf("no more test data\n");
        exit(0);
    }
    return(0);
}

int main ( void )  
{  
    unsigned char b;

    state=0; //idle

    while(1)
    {
        if(getbyte(&b))
        {
            switch(state)
            {
                case 0: //idle
                    if(b!=0xFA)
                    {
                        printf("Invalid sync pattern 0x%02X\n",b);
                        break;
                    }
                    packoff=0;
                    checksum=b;
                    packet[packoff++]=b;

                    state++;
                    break;
                case 1: //packet length
                    checksum+=b;
                    packet[packoff++]=b;

                    packlen=b;
                    if(packlen<3)
                    {
                        printf("Invalid packet length %u\n",packlen);
                        state=0;
                        break;
                    }

                    state++;
                    break;
                case 2: //payload
                    checksum+=b;
                    packet[packoff++]=b;

                    if(packoff>=packlen)
                    {
                        state=0;
                        checksum=checksum&0xFF;
                        if(checksum)
                        {
                            printf("Checksum error 0x%02X\n",checksum);
                        }
                        else
                        {
                            process_packet(packet,packlen);
                        }
                    }
                    break;
            }
        }

        //do other stuff, handle other devices/interfaces

    }
}

答案 7 :(得分:2)

状态机无处不在。状态机是通信接口中的关键,其中消息在接收时需要被解析。此外,在嵌入式系统开发中,由于严格的时序约束,我需要将任务分成多个任务。

答案 8 :(得分:2)

我在这里没有看到任何实际上解释了我看到它们使用的原因。

出于实际目的,程序员通常必须在操作过程中被迫返回一个线程/退出权限时添加一个。

例如,如果您有多状态HTTP请求,则可能具有如下所示的服务器代码:

Show form 1
process form 1
show form 2
process form 2

问题是,每次显示表单时,您都必须退出服务器上的整个线程(在大多数语言中),即使您的代码在逻辑上一起流动并使用相同的变量。

在代码中放置并返回线程的行为通常使用switch语句完成,并创建所谓的状态机(非常基本版本)。

随着你变得越来越复杂,很难弄清楚哪些状态是有效的。人们通常会定义一个“State Transition Table”来描述所有状态转换。

我写了一个state machine library,主要的概念是你可以直接实现状态转换表。这是一个非常简洁的练习,不知道它会过得多好......

答案 9 :(得分:2)

在您拥有多个状态并且需要在刺激上转换到不同状态的任何地方都使用FSM。

(事实证明这包含了大多数问题,至少在理论上)

答案 10 :(得分:2)

许多数字硬件设计涉及创建状态机以指定电路的行为。如果您正在编写VHDL,它会出现很多。

答案 11 :(得分:2)

Regular expressions是有限状态机(或“有限状态自动机”)发挥作用的另一个例子。

编译的regexp是一个有限状态机,并且 正则表达式可以匹配的字符串集正是有限状态自动机可以接受的语言(称为“常规语言”)。

答案 12 :(得分:2)

QA基础架构,旨在筛选或以其他方式运行测试过程。 (这是我特定的经验领域;我为我的上一个雇主用Python构建了一个状态机框架,支持将当前状态推送到堆栈并使用各种状态处理程序选择方法在我们所有基于TTY的屏幕抓取器中使用) 。概念模型非常合适,因为在TTY应用程序中运行时,它会经历有限数量的已知状态,并且可以移回旧的状态(考虑使用嵌套菜单)。这已经发布(经雇主许可);如果您想查看代码,请使用Bazaar查看http://web.dyfis.net/bzr/isg_state_machine_framework/

票证,流程管理和工作流程系统 - 如果您的票证有一套规则确定其在新,TRIAGED,进行中,需要QA,FAILED-QA和验证(例如)之间的移动,那么'我们有一台简单的状态机。

构建小型,易于推广的嵌入式系统 - 交通信号灯是一个关键的例子,其中所有可能状态的列表已经被完全枚举和已知。

解析器和词法分析器是基于状态机的,因为确定流入的方式取决于您当时的位置。

答案 13 :(得分:1)

我有一个我正在研究的当前系统的例子。我正在建立股票交易系统。跟踪订单状态的过程可能很复杂,但如果您为订单的生命周期构建状态图,则会使新的传入事务更加简单地应用于现有订单。如果您从当前状态知道新事务只能是三件事中的一件而不是20件中的一件,则应用该事务所需的比较要少得多。它使代码更有效率。

答案 14 :(得分:1)

有限状态机可用于任何自然语言的形态解析。

理论上,这意味着形态和语法在计算层之间被分开,一个最多是有限状态,另一个最温和地是上下文敏感的(因此需要其他理论模型来解释词到 - 而不是语素与语素之间的关系。)

这在机器翻译和单词上光方面很有用。从表面上看,它们是低成本的功能,可以在NLP中提取不那么简单的机器学习应用程序,例如语法或依赖项解析。

如果您有兴趣了解更多信息,可以查看Beesley和Karttunen的有限状态形态,以及他们在PARC设计的Xerox有限状态工具包。

答案 15 :(得分:0)

状态驱动代码是实现某些类型逻辑的好方法(解析器就是一个例子)。它可以通过多种方式完成,例如:

  • 状态驱动在给定点实际执行的代码位(即状态隐含在您正在编写的代码段中)。 Recursive descent parsers是此类代码的一个很好的例子。

  • 状态驱动在条件中执行的操作,例如switch语句。

  • 显式状态机,例如由LexYacc等解析器生成工具生成的状态机。

并非所有状态驱动的代码都用于解析。通用状态机生成器是smc。它吸入状态机的定义(用其语言),它将以各种语言为状态机吐出代码。

答案 16 :(得分:0)

好的答案。这是我的2美分。有限状态机是一种理论思想,可以通过多种不同的方式实现,例如表,或作为while-switch(但不要告诉任何人它是一种说goto 恐怖的方式)。这是一个定理,任何FSM对应于正则表达式,反之亦然。由于正则表达式对应于结构化程序,因此您可以有时只编写结构化程序来实现FSM。例如,一个简单的数字解析器可以写成:

/* implement dd*[.d*] */
if (isdigit(*p)){
    while(isdigit(*p)) p++;
    if (*p=='.'){
        p++;
        while(isdigit(*p)) p++;
    }
    /* got it! */
}

你明白了。并且,如果有一种运行速度更快的方式,我不知道它是什么。

答案 17 :(得分:0)

典型的用例是红绿灯。

在实现说明中:Java 5的枚举可以有抽象方法,这是封装状态相关行为的绝佳方法。