<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          狀態(tài)機漫談——switch:你的狀態(tài)機初戀

          共 5136字,需瀏覽 11分鐘

           ·

          2021-02-23 11:29

          (本文撰寫于2021年情人節(jié))


          【說在前面的話】


          在前面的一篇文章從零開始的狀態(tài)機漫談(1)——萬物之始的語言中,我們介紹了狀態(tài)機在整個計算機科學中宛如“世界基石”般的地位,同時介紹了一種“面向嵌入式環(huán)境”“高度簡化”了的實用型狀態(tài)圖繪制方法——這里的“簡化”是相對UML狀態(tài)圖的“繁雜”而言、且更接近課本上所使用的狀態(tài)機圖例;而這里的“實用”體現(xiàn)在:基于這套方法繪制的狀態(tài)圖是可以“無腦”而“嚴格”的翻譯成C語言代碼的


          在展開后續(xù)內(nèi)容之前,不得不為大家解釋清楚一個非常具有誤導性的錯誤認知,即:狀態(tài)機天然是非阻塞(non-blocking)的,因而可以用于在裸機狀態(tài)下實現(xiàn)多任務。實際上,這種說法后半段是正確的,錯就錯在前半部分,比如,就前一篇文章中所提到的一個狀態(tài)圖:

          翻譯成下面的C語言代碼,在邏輯上毫無問題:
          #include #include 
          void print_hello(void) { //! 對應 start部分????uint8_t?*s_pchSrc?=?"Hello";
          do {????????????//!?對應?Print?Hello?狀態(tài)????????while(!serial_out(*s_pchSrc));????????????????//! serial_out返回值為true的狀態(tài)遷移????????s_pchSrc++;????????????????//! 對應 "Is End of String"狀態(tài)????????if (*s_pchSrc == '\0') {????????????//!?true分支,結(jié)束狀態(tài)機????????????return ;????????}????????//!?false分支,跳轉(zhuǎn)到 "Print Hello" 狀態(tài)????} while(true);}
          怎么樣?發(fā)現(xiàn)之前說法的錯誤之處了吧?——是的,狀態(tài)機(狀態(tài)圖)所描述的邏輯與翻譯后的代碼是否具有“非阻塞”的特性是無關(guān)的——翻譯的方式不同,代碼的特性也不同——但無論使用何種翻譯方式,只要翻譯是正確的,最終代碼所對應的“狀態(tài)機邏輯”就是“等效”的,比如,上面的狀態(tài)機也可以翻譯成如下的非阻塞形式:
          #include #include 
          typedef enum {????fsm_rt_err = -1,????fsm_rt_on_going = 0,????fsm_rt_cpl?=?1,} fsm_rt_t;
          #define?PRINT_HELLO_RESET_FSM()?\ do {s_tState = START;} while(0)
          fsm_rt_t print_hello(void){ static enum { START = 0, PRINT_HELLO,????????IS_END_OF_STRING, } s_tState = {START};????????static?const uint8_t?*s_pchSrc = NULL;????????switch?(s_tState) {????????case START:????????????//!?這個賦值寫法只在嵌入式環(huán)境下“可能”是安全的????????????s_pchSrc?=?"Hello?world";?????????? s_tState++;???????? //break;????????case PRINT_HELLO:????????????if?(!serial_out(*s_pchSrc)) {???????????? break;?????????????????????};????????????s_tState = IS_END_OF_STRING; s_pchSrc++;???????????? //break;????????????????????case IS_END_OF_STRING:????????????if?(*s_pchSrc?==?'\0') {????????????????PRINT_HELLO_RESET_FSM();????????????????return?fsm_rt_cpl;????????????} s_tState = PRINT_HELLO; break;????????????????} return fsm_rt_on_going;}

          對比兩個代碼,可以清楚的發(fā)現(xiàn)這兩個事實:

          • 在狀態(tài)機邏輯層面,兩個代碼都正確的翻譯(表達)了狀態(tài)圖的邏輯

          • 在C代碼的實際執(zhí)行層面,一個是“不完成任務就絕不回來”的阻塞代碼;一個是在狀態(tài)執(zhí)行間隙還會“悄悄”退出函數(shù)——釋放處理器的非阻塞代碼


          所以說,與上述情況類似,市面上不少關(guān)于狀態(tài)機的說法其實都是“有待商榷”、甚至是“錯誤的”,比如:

          • 狀態(tài)機天然的是非阻塞代碼;

          • 因為狀態(tài)機經(jīng)常切換,因此實時性好;

          • 狀態(tài)機經(jīng)常切換,沒法以最快的速度響應事件,所以實時性差;

          • 狀態(tài)機執(zhí)行效率低下;

          • 狀態(tài)機執(zhí)行效率高;

          • 狀態(tài)機占用代碼空間大;

          • 狀態(tài)機占用資源小,適合資源有限的小單片機;

          • 任何狀態(tài)機都可以翻譯成普通的RTOS任務(注意,這里的說法強調(diào)的不是不是狀態(tài)機代碼在RTOS任務里執(zhí)行,而是把狀態(tài)圖翻譯成RTOS任務)

          • ……


          相信上述諸多誤解和偏見中一定有一款是讓你大為吃驚的。然而,如果你認為我這里列舉出來的說法都是“錯誤的”,那么你就又錯了


          這里的要點是——以上說法并不是“非黑即白”的,而是來源于某一些具體的狀態(tài)機翻譯方式,錯就錯在把某一種狀態(tài)機翻譯方式所具有的優(yōu)點/缺點當成了整個狀態(tài)機固有的優(yōu)點/缺點——脫離了具體的狀態(tài)機翻譯方式,從而導致了“不準確”


          說了這么多,無非就是想讓你們知道以下幾點:
          • 狀態(tài)機/狀態(tài)圖的翻譯方式眾多;

          • 不同翻譯方式在代碼的行為特性上存在天壤之別;

          • 拋開具體翻譯方式談狀態(tài)機特性都是耍流氓

          • 如果說狀態(tài)圖才是“新的源代碼”,翻譯C代碼就是“新的匯編”,根據(jù)一定規(guī)則翻譯狀態(tài)圖為C代碼的過程就是”新的編譯“。


          下面我們就以大部分人第一次接觸和使用狀態(tài)機時常用的 switch 狀態(tài)機為例,為大家介紹前一章所屬狀態(tài)圖的翻譯規(guī)則。

          讓我們上路吧!


          (本文撰寫于2021年情人節(jié))


          【狀態(tài)函數(shù)返回值的“小心思”】


          對很多人來說,即便狀態(tài)機“初戀”不是使用switch編寫的函數(shù),也一定逃不開使用函數(shù)作為狀態(tài)機載體的形式(比如使用大量if-else作為基礎(chǔ)的狀態(tài)機)。觀察狀態(tài)圖,你會發(fā)現(xiàn)狀態(tài)機是有返回狀值的:

          比如圖中右上角的“on-going”和右下角的“cpl”,分別表示狀態(tài)機“正在工作(on-going)”和“已經(jīng)完成(complete)”。圖上的狀態(tài)機算是比較簡單的了,其它狀態(tài)機可能還有返回其它信息的需求——比如,一個接收字符的狀態(tài)機可能還需要返回“超時(timeout)”這樣的信息——因此,定義一個專門的枚舉類型來作為狀態(tài)機函數(shù)的返回值就顯得非常有必要:

          typedef?enum {    fsm_rt_on_going,    fsm_rt_cpl,} fsm_rt_t;

          到了這里,有一個細節(jié)問題需要考慮,fsm_rt_on_goingfsm_rt_cpl分別對應怎樣的具體值好呢?(或者干脆不管?)。要解決這個問題,實際上只有是站在狀態(tài)機函數(shù)用戶角度考慮進行考慮,才能找到不會違反用戶直覺(屁股決定腦袋)的答案。從狀態(tài)機調(diào)用者的角度來看,既然我們告訴TA狀態(tài)機函數(shù)是非阻塞的,那么用戶最關(guān)心的最基本問題恐怕就是:


          • 狀態(tài)機是否執(zhí)行完成了?

          • 狀態(tài)機有沒有遇到什么自己不能處理的錯誤?


          對于第一個問題,顯然其答案是一個布爾量:

          • 如果返回false,則表明狀態(tài)機還沒有執(zhí)行完成——需要繼續(xù)執(zhí)行(on-going);

          • 如果返回true,則表明狀態(tài)機已經(jīng)執(zhí)行完成(complete)


          基于這樣的原因,完全可以根據(jù) <stdbool.h> 中的定義,給我們的 fsm_rt_t 一個兼容的值,即:

          typedef?enum {    fsm_rt_on_going   = 0,????fsm_rt_cpl????????=?1,} fsm_rt_t;

          對于第二個問題,實際上,程序員之間有一個不成文的規(guī)定,即:錯誤碼用負數(shù)表示,因此,我們可以引入一個“不問緣由的默認的錯誤碼” (-1),并允許用戶可以用除去(-1)以外的其它負數(shù)來編碼更為具體的錯誤——這里就把這種自由度留給用戶自己去發(fā)揮了,我們只需要在 fsm_rt_t 中引入(-1)就可以了:

          typedef?enum {????fsm_rt_err????????= -1,    fsm_rt_on_going   = 0,????fsm_rt_cpl????????=?1,} fsm_rt_t;

          至此,我們完成了一個狀態(tài)機返回值的定義過程,并隱含了以下的規(guī)則:

          • 對于“確定”不會返回錯誤碼的狀態(tài)機函數(shù)來說,狀態(tài)機函數(shù)的使用與bool量是兼容的;

          • 用戶可以使用負數(shù)來“自定義”錯誤碼,并使用(-1)表示“不問緣由的默認錯誤碼”;


          需要特別強調(diào)的是,錯誤碼表示發(fā)生了“狀態(tài)機發(fā)生了預期之外、無法繼續(xù)正常工作的情況”,比如,狀態(tài)機函數(shù)需要一個指針,但你傳了一個空指針;或是狀態(tài)機函數(shù)收到了一個無效的輸入?yún)?shù),導致后續(xù)工作都無法正常執(zhí)行,等等。


          • 用戶定義的其它狀態(tài)值,比如超時之類的,它們必須是大于(1)的正數(shù)。


          與錯誤碼不同,這類用返回值是狀態(tài)機正常工作的結(jié)果,屬于狀態(tài)機邏輯本身所能預期和處理的。所以,哪怕“超時”聽起來像是一個“錯誤”,但它本質(zhì)上還是狀態(tài)機邏輯所預期會發(fā)生并能正確檢測和處理的,因此并不會作為一個負數(shù)錯誤碼來返回。


          在這個系列后面的文章中,我們還會引入兩個默認的正整數(shù)狀態(tài)返回值到 fsm_rt_t這里就先不贅述了:
          //!?\name?finit?state?machine?return?value//! @{typedef enum {    fsm_rt_err          = -1,    //!< fsm error, error code can be get from other interface    fsm_rt_cpl          = 0,     //!< fsm complete    fsm_rt_on_going     = 1,     //!< fsm on-going    fsm_rt_wait_for_obj = 2,     //!< fsm wait for object    fsm_rt_asyn         = 3,     //!< fsm asynchronose mode, you can check it later.} fsm_rt_t;//!?@}


          借助 fsm_rt_t 類型的幫助,我們的狀態(tài)機函數(shù)終于有了一個像樣的外殼,比如:
          fsm_rt_t?<狀態(tài)機函數(shù)的名字>([形參列表]){????...    return fsm_rt_on_going;    //!< 默認的返回值}


          為了方便大家的理解,我們就以“帶超時功能的字符接收狀態(tài)機”為例子,為大家介紹對應的狀態(tài)圖繪制方法以及對應的代碼片段:

          觀察上圖可以發(fā)現(xiàn),狀態(tài)機read_byte會在讀取字符的同時進行一個簡單的倒計數(shù);如果在s_wCounter0之前成功讀取到了一個字節(jié),則返回cplpchByte所指向的字節(jié)buffer將保存對應的字節(jié));如果讀取字節(jié)失敗,但計數(shù)器還未到零,則返回 on_going——表明狀態(tài)機還在工作中;如果計數(shù)器到達了0,則返回一個自定義的狀態(tài)信息(timeout),用以表明發(fā)生了超時。在圖中,不光矩形框內(nèi)部多了一個名為 timeout 的黑色小圓點;在矩形框的外部(右側(cè))也出現(xiàn)了一個對應的扇出箭頭,同樣也標記了 timeout——這實際上是告訴我們,當狀態(tài)機遷移到?timeout 終點時,將通過 timeout 箭頭扇出,而狀態(tài)機也將復位


          它對應的一個可能代碼為:
          enum {????fsm_rt_timeout?=?4,???? //!};
          #ifndef TIMEOUT_CNT#???define?TIMEOUT_CNT????(1000000ul)#endif
          extern?bool?serial_in(uint8_t?*pchByte);
          #define READ_BYTE_RESET_FSM() \ do {s_tState = START;} while(0)fsm_rt_t read_byte(uint8_t *pchByte){ static enum { START = 0, READ_BYTE, IS_TIMEOUT, } s_tState = {START}; static uint32_t s_wCounter; if (NULL == pchByte) { READ_BYTE_RESET_FSM();????????return?fsm_rt_err;???//! }
          ????switch?(s_tState) {???? case START: s_wCounter = TIMEOUT_CNT; s_tState++; //break;????????case READ_BYTE:???????? if (serial_in(pchByte)) {????????????????READ_BYTE_RESET_FSM();????????????????return fsm_rt_cpl;???????? }????????????????????????s_wCounter--;????????????s_tState = IS_TIMEOUT;????????????//break;????????????????????case IS_TIMEOUT:???????? if (0 == s_wCounter) {???????? READ_BYTE_RESET_FSM();???????? return (fsm_rt_t) fsm_rt_timeout;???????? }???????? s_tState = READ_BYTE;????????????break;????}???????? return fsm_rt_on_going;}

          這個代碼有幾個細節(jié)值得大家注意:

          • fsm_rt_timeout 是一個額外定義的枚舉,其實我們并不需要給它配備一個所謂的類型——畢竟只是拿它當一個常數(shù)用,直接用匿名枚舉就行了;

          • fsm_rt_timeout?本質(zhì)上是屬于匿名枚舉的,因此作為兼容 fsm_rt_t?的值返回時,有些編譯器還是會報告?warning——提示我們返回值并不是 fsm_rt_t 的一部分——這里我們直接使用強制類型轉(zhuǎn)換讓編譯器“閉嘴即可”;

          • 狀態(tài)函數(shù)需要用戶傳入一個指針 pchByte,容易發(fā)現(xiàn),如果傳入值是NULL,整個狀態(tài)機就無法正常工作了,因而視作錯誤,需要返回負數(shù)錯誤碼;又由于這里我們很懶,沒有定義專門定義這一情況的錯誤碼,因此以 fsm_rt_err 來湊數(shù)。一般來說錯誤碼的返回值是不用在狀態(tài)圖上進行明確標注的


          【不要小看了狀態(tài)的定義】


          與返回值類似,狀態(tài)機的狀態(tài)也可以用枚舉來定義,但這里有一些細節(jié)是需要注意的:
          • 由于定義狀態(tài)的枚舉實際上是狀態(tài)機函數(shù)的“私有財產(chǎn)”,也就是說只有狀態(tài)機函數(shù)會“使用且只用一次”,因此:

            • 沒有必要為其使用 typedef 來定義一個類型;

            • 應該放在狀態(tài)機函數(shù)的內(nèi)部——由花括號限制枚舉的作用范圍;

            • 由于這一枚舉類型的作用范圍被限制在了函數(shù)內(nèi)部,因此狀態(tài)機之間不存在“重名”或者“命名空間污染”的問題——換句話說,

              • 每個狀態(tài)的名稱都可以盡可能的簡單;

              • START在每個狀態(tài)機函數(shù)里都可以被定義一次,而且永遠叫START

          • 狀態(tài)的命名上應該盡可能以狀態(tài)圖上的狀態(tài)名為“藍本”;

          • 狀態(tài)名應該盡可能的有意義,而不是像STATE_ASTATE_B, ... STATE_X 這樣“用一個英文字母序號”去代表“0,1,2...n這樣的數(shù)字序號”——二者無論是誰都沒有為“狀態(tài)是做什么的”提供任何有意義的信息。相對的,例如 READ_BYTEIS_TIMEOUT 這樣的名稱就非常簡潔明了。


          以前面read_byte狀態(tài)機代碼為例,一些錯誤的或者說不推薦的做法為:

          //!//!typedef enum {????FSM_RB_START?=?0,??//!????FSM_RB_STATE_A,????//!    FSM_RB_STATE_B,} read_byte_state_t;
          fsm_rt_t read_byte(uint8_t *pchByte){ static read_byte_state_t s_tState = {FSM_RB_START};????...}

          作為對比,正確的做法如下:

          fsm_rt_t read_byte(uint8_t *pchByte){    static enum {        START = 0,        READ_BYTE,        IS_TIMEOUT,    } s_tState = {START};????...}


          【START不是狀態(tài)】


          如果你認真閱讀從零開始的狀態(tài)機漫談(1)——萬物之始的語言并觀察狀態(tài)圖會發(fā)現(xiàn):START是狀態(tài)機的起點、同時也兼任躍遷條件——換句話說:
          • START 不是一個可以保持的狀態(tài),它也不能被看作一個特殊的狀態(tài);因此,翻譯代碼的時候,雖然START是0,但在對應的case分支中,一定要自動切換到下一個狀態(tài)而絕對不能在此停留——這就是紀律!

          • 另外一個“START不能被當做狀態(tài)來使用”的原因是,start作為一個躍遷條件,它是可以擁有“發(fā)生躍遷時執(zhí)行且只執(zhí)行一次的動作的”——又由于START是處于復位狀態(tài)的狀態(tài)機第一次執(zhí)行時的起點,因此START所攜帶的執(zhí)行動作一般用作狀態(tài)機的初始化——比如初始化狀態(tài)機所使用的變量等等

          • 如果狀態(tài)機需要動態(tài)申請資源,比如malloc,考慮到失敗的可能,如果允許重試,則這類資源分配代碼就不能放置在START中,因為我們說過,START不是狀態(tài)——在狀態(tài)機復位之前不應該重復執(zhí)行;如果分配失敗被視作錯誤,會返回負數(shù)的錯誤碼,并復位狀態(tài)機,則允許將這類資源分配代碼放置到START中——因為邏輯上我們遵守了規(guī)則。


          作為例子,不要嘗試干出這種事情:

          fsm_rt_t example(...){    static enum {        START = 0,        ...
          } s_tState = {START};????static?uint32_t?s_pchArray;

          ????switch?(s_tState) {???? case START: s_pchArray = malloc(64); if (NULL == s_pchArray) { break; } s_tState++;????...}



          應該專門給這類允許重試的資源分配一個獨立的狀態(tài):


          fsm_rt_t example(...){    static enum {        START = 0,????????MALLOC,        ...    } s_tState = {START};????static?uint32_t?s_pchArray;
          ????switch?(s_tState) {???? case START:????????????s_tState++;????????????//break;????????case MALLOC: s_pchArray = malloc(64); if (NULL == s_pchArray) { break; } s_tState = XXXXX; break; ????...}


          【如何實現(xiàn)從狀態(tài)到代碼的“無腦翻譯”】


          經(jīng)過了這么多的準備工作,我們終于進入到具體狀態(tài)的翻譯這一環(huán)節(jié)中了。事實上,狀態(tài)的翻譯比你想象的要簡單,針對下面的一個狀態(tài)示意圖:

          它可以簡單的對應到下面的代碼結(jié)構(gòu):

              case <狀態(tài)名稱>:????????狀態(tài)具體執(zhí)行了什么有返回值d的動作;????????if?(返回值?滿足?躍遷條件1) {????????????s_tState?=?XXXXX;???//!????????????執(zhí)行對應的躍遷動作????????}?else?if?(返回值?滿足?躍遷條件2) {            s_tState = XXXXX;   //!< 執(zhí)行狀態(tài)躍遷            執(zhí)行對應的躍遷動作????????????????}????????break;

          一般來說,我們既可以用上面的公式無腦翻譯代碼,也可以進行必要的等效改編。比如,對于READ_BYTE狀態(tài):

          我們可以無腦翻譯成如下的代碼:

          ????????case READ_BYTE:????????    if (serial_in(pchByte)) {????????????????READ_BYTE_RESET_FSM();????????????????return fsm_rt_cpl;????????    }????????????????????????s_wCounter--;????????????s_tState = IS_TIMEOUT;????????????break;??


          如果我在這里說,狀態(tài)的翻譯并不復雜,一些小伙伴可能會“哼”的冷笑一聲,順手甩出一個“王炸”——“如果一個狀態(tài)很復雜怎么辦”?對于這個問題,我的答案是:
          • 如果你的狀態(tài)很復雜,那么一定可以拆分成多個狀態(tài)彼此配合的形式;

          • 拆分后每個狀態(tài)都應該功能單一;

          • 拆分后的邏輯應該更加清晰;


          所以,不要問我“一個狀態(tài)很復雜怎么翻譯”,先看看你是不是做了所謂的“超級狀態(tài)”——嘗試把很多事情都在一個狀態(tài)里做了——如果發(fā)生了這種事情,請反思這跟“把所有應用代碼都寫在超級循環(huán)里,而且還不涉及函數(shù)調(diào)用”有啥區(qū)別。最后,關(guān)于把“超級狀態(tài)”拆分成多個簡單狀態(tài)的組合以后可能面臨的“所謂”性能優(yōu)化問題,我們將在本系列后面的文章從零開始的狀態(tài)機漫談(3)——狀態(tài)機設計原則:清晰!清晰!還是清晰!為您詳細介紹,敬請期待。


          【復位是一門大學問】


          讀到這里,很多小伙伴可能已經(jīng)在前面的代碼中發(fā)現(xiàn)了如下的細節(jié):
          #define READ_BYTE_RESET_FSM() \    do {s_tState = START;} while(0)

          或是:

          #define?PRINT_HELLO_RESET_FSM()?\    do {s_tState = START;} while(0)

          于是心中升起了疑問:如果復位就是把狀態(tài)變量重新設置為 START

          • 為什么不直接在圖上所有要復位的地方直接畫一條箭頭——躍遷到到第一狀態(tài)?

          • 為什么不定一個統(tǒng)一的宏,比如叫 RESET_FSM() 就好了,而是給每個狀態(tài)機都定義一個自己的宏?


          要回答第一個問題并不困難:

          • 復位并不是普通的狀態(tài)躍遷,它表示將狀態(tài)機“重置”——復位后的第一次執(zhí)行,狀態(tài)機會從START那里開始,并且完成必要的狀態(tài)機初始化操作;

          • 統(tǒng)一采用START作為狀態(tài)機的起點,可以避免第一個狀態(tài)出現(xiàn)恐怖數(shù)量的扇入箭頭,從而極大的簡化了狀態(tài)圖(你也不想看到蜘蛛網(wǎng)一樣密集的箭頭吧);

          • 避免了每個扇入的躍遷所擁有的“初始化代碼”可能會存在“不同”而導致的代碼陷阱——因為我們統(tǒng)一從START進入,因此只要維護一份初始化代碼就足夠了。


          對于第二個問題,我們要從更長遠的角度來考慮:現(xiàn)階段的狀態(tài)機也許很簡單,所以復位僅僅是重置狀態(tài)變量就夠了;然而,隨著應用結(jié)構(gòu)的復雜,以及狀態(tài)機翻譯方式的改進或者變化,每個狀態(tài)機函數(shù)所需的復位操作可能都是不同的,因此從養(yǎng)成好習慣的角度出發(fā),應該給每一個狀態(tài)機都配備一個專屬的復位宏


          很多小伙伴在編寫狀態(tài)機的時候,可能會有這樣一類要求:即,出于某種原因,應用程序的某些模塊需要“從外部”復位某些狀態(tài)機,換句話說——就是殺死狀態(tài)機——這其實很類似RTOS里面,殺死某個任務線程的情況。對此我要說說我的看法:
          • 首先,應該盡最大可能避免從狀態(tài)機外部復位狀態(tài)機,或者說,狀態(tài)機的生命周期應該掌控在自己手里。這么做的原因很簡單,也很關(guān)鍵,即理論上沒有任何人比狀態(tài)機自己更清楚如何安全而有效的復位一個狀態(tài)機。如果這么說你不能理解,考慮如下幾種情況:

            • 狀態(tài)機中可能存在動態(tài)分配的資源,狀態(tài)機自己內(nèi)部的復位過程中會正確的釋放這些資源;而來自外部的“它殺”在殺手掌握的信息不充分的情況下,可能會導致這類資源未被正確釋放

            • 狀態(tài)機非常適合用作各類機械控制,然而,出于機械機構(gòu)的特殊原因,為了防止損害設備,或者傷害到人員,這類狀態(tài)機都會根據(jù)當前的工作狀態(tài),有一套針對性的(通常是不同的)復位序列(甚至某些狀態(tài)下根本不允許復位),而且復位過程本身也是需要時間的,因此在這種情況下直接由外部進行“他殺”實際上是不可承受之重

          • 其次,應該用“自殺請求”來代替“直接他殺”,即:狀態(tài)機在設計時即提供一個“復位請求”信號,并在狀態(tài)機內(nèi)部適當?shù)臓顟B(tài)檢測這一信號;外部應用只能通過這一信號來“請求”狀態(tài)機復位;當復位成功后,狀態(tài)機應該通過某種手段,比如特定的返回值或者回調(diào)函數(shù)來告知請求者“復位完成”。



          【細數(shù)那些絕對要杜絕的“騷操作”】


          在設計狀態(tài)機或者翻譯switch狀態(tài)機的過程中,以下常見“騷操作”是應該避免的:
          • 在一個函數(shù)里塞入多個switch狀態(tài)機實現(xiàn)——請記住,每個switch狀態(tài)機都應該有自己專屬的一個函數(shù);?

          • 在 switch 外部添加各類功能性的代碼。這種做法,本質(zhì)上就是模擬了“多線程”,也就是switch狀態(tài)機邏輯被看作一個“線程”、switch外部的功能代碼客觀上就充當了另外一個“線程”。這種情況完全可以通過將兩份代碼拆分到獨立的任務函數(shù)中,并以某種形式的“任務間通信”完成協(xié)調(diào)——最終實現(xiàn)一樣的功能

          • 把狀態(tài)變量定義到狀態(tài)機函數(shù)外部,從而方便別人“偷窺”或者“復位”——請參考前面一個章節(jié)的內(nèi)容,用“自殺”替代“它殺”。



          【后記】


          相信對很多人來說,switch狀態(tài)機都是它們裸機環(huán)境下的“制勝法寶”,我并不準備否認這一點,相反,我希望通過這篇文章,能夠分享一下我在使用switch方式翻譯狀態(tài)圖的一些做法以及背后的思考。
          希望大家不要誤解我——認為我這里介紹的方法就是 switch 狀態(tài)機編寫方式的“權(quán)威”,很遺憾的是,如果你有這種想法,那么我在本文開頭處所作的努力就化為烏有了——也許狀態(tài)圖的所表達的邏輯是唯一的,但翻譯它的方法從來都不是唯一的;同時每一個方法都有自己的利弊,希望大家在討論喜好的時候,不要動輒就把某一類方法的特點強加到“狀態(tài)機”整體身上加以評判。




          原創(chuàng)不易,

          如果你喜歡我的思維、覺得我的文章對你有所啟發(fā),

          請務必 “點贊、收藏、轉(zhuǎn)發(fā)” 三連,這對我很重要!謝謝!


          歡迎訂閱 裸機思維


          瀏覽 126
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  久久婷婷丁香五月 | 影音先锋电影成人AV | 狠狠躁日日躁夜夜躁A片无码 | 日韩三级黄片 | 91豆花成人 |