返回留言板
文章标题:把状态机呈现给用户是拙劣的设计
文章作者:bluesen
发表时间:2005-2-22 9:55:59
内容:
把状态机呈现给用户是拙劣的设计
--从历史角度再论“状态机”

bluesen /文
2005.1.12


    在IVR设计中,在早期采用状态机是无奈的选择,对应用程序的开发者而言(以下也称“用户”),状态机实际上是很难理解的概念,也是造成IVR设计复杂性的根源。不过这也给Intel CT ADE、蓝星际Koodoo语言一类的IVR开发工具带来了巨大的市场空间。
    提供给用户类似高级语言而非状态机的用户界面,给用户带来了很大的便利,但要实现这么一个带有强大编译功能的语音平台并不容易,其难度远远超过状态机实现,需要较强的开发实力,对平台厂商是个很大的挑战。


    为什么说状态机复杂的?

    这要从IVR语音开发的原始方式说起。作为语音板卡厂商基本上提供了两种类型的API,一种是以Dialogic为代表的事件驱动模式,另外一种是以国内厂商如东进、三汇的轮询模式;当然Dialogic接口较为丰富,除了事件驱动模式外也支持轮询等模式。
    大家也许没有意识到,传统方法上采用这两种模式的API开发应用程序其实都是状态机。
    所谓状态机(或有限状态机,即FSM),是指“用一组可能的状态来描述系统行为,系统在任何时刻只能处于其中的一个状态。也可以描述由输入值决定的状态转移。最后可以描述在某个状态下或状态转移期间可能发生的操作。”

    先来看看一段典型的Dialogic例子程序:
    (有关Dialogic的代码均摘自msidemo.c,版权属于Intel公司)
TABLE table[]=
  {/*current_state event       next_stat   function */
  { ST_WTRING,     DE_RINGS,   ST_OFFHOOK, setoffhk  },
  { ST_OFFHOOK,    DX_OFFHOOK, ST_PLAY,    play      },
  { ST_OFFHOOK,    DE_LCOFF,   ST_ONHOOK,  sethook   },
  { ST_PLAY,       TM_EOD,     ST_GETDIG,  get_digits},
  { ST_PLAY,       TM_MAXDTMF, ST_GETDIG,  get_digits},
  ...
  };
    这个结构描述了一个状态机,每一个状态都有状态名字如ST_WTRING,
事件如DE_RINGS,本状态完成后即将转移的下一个状态如ST_OFFHOOK,本状态对应的动作(函数)如setoffhk等等。作为一个最简单演示基本功能的程序其状态就有55个之多。
    驱动这些事件的核心是check_event()函数, 循环调用下列代码:
    if(dxinfo[channel].state == table[i].current_state
          && event == table[i].event){
       // 找到当前状态下对应的动作
       func_ptr = table[i].funcptr;
       dxinfo[channel].state = table[i].next_state;
       (*func_ptr)(channel);  // 执行这个动作
       ...
     }
     而执行的动作之中会根据情况,改变通道的状态。
     请注意,因为是多线路并发执行,所以几乎任何语音操作都是异步的,不允许任何的堵塞。

    好,我们再看看东进公司的一段例子程序:
    (有关东进的代码均摘自Dial\D.c,版权属于东进公司)
void WINAPI yzDoWork()
{
  ...
  for(int Line=0;Line<TotalLine;Line++){
     yzDrawState(Line);  //draw
     switch(Lines[Line].State){  //state transfer
        case CH_FREE:
           break;
        case CH_DIAL:
           if(CheckSendEnd(Line) == 1){
              StartSigCheck(Line);
              Lines[Line].State=CH_CHECKSIG;
           }
           break;
        case CH_CHECKSIG:
           tt = Sig_CheckDial(Line);
           if(tt == S_BUSY)
              Lines[Line].State = CH_BUSY;
           else if(tt == S_CONNECT)
              Lines[Line].State = CH_CONNECT;
           else if(tt == S_NOSIGNAL)
              Lines[Line].State= CH_NOSIGNAL;
           break;
        case CH_BUSY:
        case CH_NOSIGNAL:
           ...
     }
  }
}

    这也是一个典型的状态机,标识了很多状态,然后在每个状态下执行响应的操作,并且改变其状态--迁移到下一个状态。

    采样这种状态机的理由是,语音系统往往通道很多,每个通道看起来是并发操作,所以最简单是实现就是每个通道保存自己当前的状态,并进行迁移。
因为只有一个控制线程(或进程),所以每个状态下的操作不允许堵塞,如果某个线程执行一个耗时半分钟的操作,其它所有的线路将会同时引起停顿。
    我们也可以把状态看成是个时间片,你必须精心地划分好时间片,让操作足够地短。这类似早期的Windows3.x操作系统,是非抢占式的,所以把状态看成是命名的消息也是可以的。这类系统总有一个事件处理函数,去处理这些系统消息或用户消息(状态)。

    开发者为什么普遍觉得这样的程序难写?
    首先,如果应用复杂,状态是非常多的,经常达到数千个,开发者要仔细地划分这些状态是很大的工作量。
    其次,这些状态混在一起,没有层次,很难管理。因为这所有的状态地位都是平等的,是线性关系。这样的代码实际上也很难维护,造成了语音开发的门槛。
    第三,因为上述第二点的原因,业务操作的代码也只好和语音操作的代码混在一起,并且要强行对业务代码进行也进行状态划分,还需要小心避免业务的长操作。
    第四,造成应用开发人员被迫进行底层思维,比如一个放音操作,要人为地分解为1、开始放音,2、判断有没有放完,3、有没有被按键打断等等。
    第五,当线路较多时,容易造成性能的急剧下降。这主要是循环处理造成的。
    第六,流程的可读性变差,因为状态可以随意跳转而由于处理是线性结构很难看出流程的实际走向。
    第七,很难单步跟踪调试。
    第八,不容易以直观的方式实现循环,而很多业务实际上是需要限制次数的,如密码不对后的几次身份验证,重复播音次数,语音功能菜单最多操作次数等。

    目前市面上以新太为代表的语音平台产品,还是以状态机为核心,上述弊端基本上都存在,给客户带来的唯一方便就是避免了对底层板卡API编程,思维方式并没有变化。
    笔者在以前撰文指出过的新太脚本形同汇编,就是其死抱状态机教条带来的恶果,再怎么图形化也没有用。

    实际上,现代操作系统的发展和语音板卡API的发展给语音开发带来了全新的编程模式,这就是基于多线程的编程模式。这种编程思想的最基本出发点就是,把单一的通道限定在单一的线程之中执行,这样完全可以不必考虑时间片、消息、状态等额外的东西,语音操作也可以使用同步堵塞操作了,既符合程序员的思维,也符合业务流程的自然流向,并且可以彻底实现底层操作和业务操作的分离。
    以Koodoo语言为例子(Intel CT ADE类似),每个语音通道相当于一个虚拟机,虚拟机执行以Koodoo语言编制的业务流程脚本。而Koodoo语言具有现代高级语言的特性。这样彻底摆脱了状态机的桎梏,实现了语音开发的根本性的变化。

已有回复:


回复如下

标题:
    发言人:

内容: [回复] [重写]