1. 程式人生 > >最佳實踐之有限狀態機

最佳實踐之有限狀態機

有限狀態機(Finite State Machine,FSM),簡稱狀態機。今天這篇文件的主體思路,來自本人授權的一項發明專利。第一次嘗試寫出來,希望分享給更多人。

我當時寫這個專利的時候,太有感覺了。非常的激動,同時我也很想分享給同事,但是可能太抽象了,未果。然後我想申請優秀專利獎,沒有渠道!所以最近刷屏的屠呦呦沒有評上院士的訊息,我聽後,心想意料之中吧。當年在學校寫論文那個叫痛苦,說實話,我真的沒感覺,後面終於東拼西湊,勉強過關。我媽說要我去讀博士,我不敢了!同學建議我去大學當老師,我也不敢,我真怕誤人子弟,我自己都懷疑自己讀的書怎麼用,完全沒多少實踐啊,雖然當時我也參加了導師的專案,用Delphi做了個介面。但是那叫實踐嗎,反正當時就是沒感覺,沒開竅。而經過這些年的摸爬打滾,幾年後分成兩次寫了幾篇專利,現在都已授權。這些專利都是非常有感覺,不是帶任務的那種,所以基本上是一氣呵成。其中有三篇是圍繞一個主題從不同角度寫的,太有感覺了。我當時想,我現在應該對得起這張文憑了!你們說如果我現在去大學當老師,會有人要嗎?我覺得難,現在當老師都要求博士了,還要留過洋的,或者博士後了吧,所以不想了。。。不過我輩仍需努力吧,說不定哪天你不是坐在評委席上,就是坐在候選人席上:)

第一次知道狀態機,還是在華為做測試,測傳統的通訊產品。從一個傳統的通訊協議裡面知道了這個名字,當時只是覺得這個名字挺好的,當時沒想太多,所以具體是哪個通訊協議我也忘了,當然還是記住了其中的基本思路。幾年後,在這裡重構我們的軟體時,在多執行緒中,創新性的運用了這個機制。非常好的一種方法,用了這種方法後基本沒有亂七八糟的Bug,而且好擴充套件,很容易加功能!而其實在之前解決死鎖的問題時,雛形就已經出來了。所以可以先閱讀下當時我是怎麼解決死鎖問題的,有助於你的理解。

而最近我在參加一些架構師公開課的時候,發現老師們都在使用這個機制了。但是看很多學員有點懵懂,所以我就想哪天把它寫出來。同時也說明了,理解和使用狀態機需要有一定的程式設計基礎,有些抽象。但讀懂了,收穫會不少!太好用了! 

 

狀態機,表示有限個狀態以及在這些狀態之間的轉移和動作等行為的數學模型。

確切的描述是它是一個有向圖形,由一組節點和一組相應的轉移函式組成。狀態機通過響應一系列事件而“執行”。每個事件都在屬於“當前” 節點的轉移函式的控制範圍內,其中函式的範圍是節點的一個子集。函式返回“下一個”(也許是同一個)節點。這些節點中至少有一個必須是終態。當到達終態,狀態機停止。

可以理解為,系統的行為如果在不同的時間或環境下,其工作不同,並且行為可以分成有限的狀態以及不重疊的程式塊時,系統顯現出了狀態行為。正是因為狀態機具有有限個狀態,所以在實際工程中可以實現。但也並不意味著只能進行有限次處理,相反,有限狀態機是閉環系統,有限無窮,可以用有效的狀態處理無窮的事務。

更通俗點,狀態機是通過有限且充分的狀態個數記錄執行緒的各種操作行為,並能在各種環境下、不同事件、不同時間的驅動下,切換到下一個合適的狀態,如此反覆。

總之,狀態機,是一種對流程控制進行抽象的方法。

我們舉一個常見的TCP連線對檔案進行讀操作的例子進行說明。

 

首先我們把整個流程分解成五個階段: Session不可用階段、Session建立階段、驗證階段、檔案處理階段、Session終止階段。

這一步很關鍵,儘量將粒度降低,抽象出盡可能多的狀態。而且也可以分層,進入檔案處理階段,又可以細分為檔案的開啟、迴圈讀、檔案關閉至少三個階段。

當然本文,主要圍繞Session層來展開描述。所以如果按正常的流程,這五步的執行情況如下圖。

    

我們很多人,可能將這五步寫在一個函式中,把所有的步驟放在一個函式中按順序執行,再加檔案處理階段一個大的while迴圈。

當然邏輯上這樣也沒有問題,但是有兩點值得改進:

(1)程式碼的可複用性基本上沒有和可讀性不佳;

(2)更主要的是沒有真正的對這個五個階段進行抽象化。

於是,有人想到了對流程控制進行修改,中間可能的失敗直接往後跳到某個階段,不再完全是順序執行,如下圖。

    

綠色的線為變更的地方。這種方法,對比前面思考的稍微多一些,但還是不夠充分。上面提出的兩點改進,並沒有實質變化。

程式的流程控制是任意一個環節都可能存在失敗,而且最好還可以重來,也就是說不一定只能往後結束或者退出,而是可以自我更正或者恢復。如下圖。

    

 

紅色的線為變更的地方。驗證階段,如果第一次失敗了,是否存在多個使用者名稱密碼呢。檔案處理階段,如果當前檔案讀取失敗,是否可以讀下一個檔案呢。

那有的人會說,問題也來了,如果都放在一個函式中,那豈不是又要加迴圈。如果兩次往回判斷,得加兩個while迴圈!這樣的推斷沒問題。但同時解決方法也是有的。

也就是回到了開始提到的兩點改進,對策如下。

首先,將所有的步驟放在一個函式裡面,這是停留在面向過程的編碼,不是面向物件。所以,第一步需要將這個五步分別建立五個方法。這樣每個方法都可以複用,即使將五個方法按順序執行,最少的程式碼可能只需五行程式碼,至少也看不到檔案處理階段的while迴圈了,程式碼量是不是大大減少。可讀性是不是更佳,一看方法名就知道在做什麼,而不需要看大段程式碼,不需要關注實現細節。

其次,將五個階段定義五個值,一個列舉變數的五個值,最好是定義五個巨集,為了可讀性。那麼加上switch和case,外面再加上一個while迴圈,注意僅僅需要一個while。是不是就可以很方便的進行狀態切換,不管是往前切還是往後切、反覆切。這就是狀態機真正的精髓!

我們再重複下狀態機的設計過程。首先將流程抽象成幾個可以重複可以複用的步驟,每個步驟儘量單獨封裝成函式或者方法。然後再對這個幾個步驟或者說方法,定義一個列舉變數,列舉值對應的巨集名稱和方法名可以一一對應。那麼再就可以在一個函式中,或者多執行緒的run函式中加上while、switch和case的流程控制,顯然每個case對應了一個方法,再根據方法的返回值再判斷下一個狀態是什麼,再進入下一次的while和下一個case,如此反覆。

我們貼一段虛擬碼,示範一下。

bool bExit = 0;
m_nStatus = P_INIT;
while ( !bExit)
{
    switch (m_nStatus)
    {
        case P_INIT:
        {
            E_P_ERR  eErr = Init();
           
             if (eErr == P_ERR_INIT)//初始化錯誤
            {        
                  m_nStatus = P_EXIT;//直接退出
            }
            else if (eErr == P_OK)   
            {        
                  m_nStatus = P_OPEN; //正常執行下一步
            }
            //省去若干程式碼
            break;
        }
        case P_OPEN:
        {
            E_P_ERR  eErr = Open();
           
             if (eErr == P_ERR_OPEN)//開啟失敗
            {        
                  //m_nStatus = P_EXIT;//可以省去,狀態未變化
                  continue;//再次嘗試開啟,或者開啟下一個檔案
            }
            else if (eErr == P_OK)   
            {        
                  m_nStatus = P_READ;//正常,可以開始讀取檔案
            }
            //省去若干程式碼
            break;
        }
        //省去若干程式碼
    }
    //省去若干程式碼
}//while
                        

 從程式碼可以看出,程式結構非常簡單,流程非常清晰,對狀態的轉換非常明確。

事實上,我們在實現中,程式碼遠不止這麼簡單。但是基本框架是類似的,更多的考慮是一些初始化、準備工作和一些異常處理工作。

而整個流程下來,你會發現這裡都用不上鎖,而且涉及到的每個物件也用不上鎖: 

對session不需要加鎖,

對檔案連結串列不需要加鎖,

對緩衝的讀寫不要加鎖,等等

最後整個流程除了維護狀態機本身的鎖以外,不需要任何鎖!

因為即使再複雜、再多的控制,對於狀態機而言,只是其中一個狀態而已!

而狀態機的鎖,主要是給外界瞭解和提供當前的狀態,所以一般也就是為了getStatus函式需要而加上,所以使用不頻繁。

爽不爽,有沒有被顛覆的感覺!:)

上一篇,提到過,儘量少用鎖,我是不是兌現了我的諾言:)

 

最後,狀態機的本質是把流程抽象化,儘量分解出可重複的步驟。最終可以大大簡化流程,提高程式碼的健壯性,基本達到無鎖實現,大大減少鎖帶來的效能消耗,提升效能;而且流程清晰,可讀性好!

狀態機適用於兩個狀態以上及需要重複使用的情況,特別適用於多執行緒。

 

 

 

推薦閱讀:

經驗總結:記憶體洩露

經驗總結:死鎖 

經驗總結:如何用巧力解決問題 

經驗總結:如何把Bug的偶現變必現

&n