從歷史角度再論“狀態機”

     在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語言具有現代高級語言的特性。這樣徹底擺脫了狀態機的桎梏,實現了語音開發的根本性的變化。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章