<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)機(jī)

          共 9388字,需瀏覽 19分鐘

           ·

          2021-10-02 00:40


          來源:裸機(jī)思維

          作者:GorgonMeducer


          【說在前面的話】


          在前面的講解中,我們介紹了如何使用狀態(tài)圖的方式來設(shè)計有限狀態(tài)機(jī)明確了狀態(tài)圖設(shè)計的“清晰”原則,并結(jié)合最簡單和常用的switch狀態(tài)機(jī)翻譯模式詳細(xì)說明了狀態(tài)圖的“無腦翻譯”方法。

          比如下面這個狀態(tài)圖就是一個典型:

          通過圖示,我們能清晰的看出該狀態(tài)機(jī)實現(xiàn)的是“通用字符串輸出”的功能。其實,這里我算是埋下了一個小小的“彩蛋”——當(dāng)然,它的真實身份是一個陷阱。如果你已經(jīng)熟悉了我前面介紹的翻譯規(guī)則,很容易就會發(fā)現(xiàn)這里存在的巨大問題:是的,這個狀態(tài)圖按照switch翻譯法無腦翻譯的后果,將是一個根本無法正常工作的狀態(tài)機(jī):
          #include <stdint.h>#include <stdbool.h>
          typedef enum { fsm_rt_err = -1, fsm_rt_on_going = 0, fsm_rt_cpl = 1,} fsm_rt_t;
          extern bool serial_out(uint8_t chByte);
          #define PRINT_STR_RESET_FSM() \ do { s_tState = START; } while(0)
          fsm_rt_t print_str(const char *pchStr){ static enum { START = 0, IS_END_OF_STRING, SEND_CHAR, } s_tState = START;
          switch (s_tState) { case START: s_tState = IS_END_OF_STRING; break; case IS_END_OF_STRING: if (*pchStr == '\0') { PRINT_STR_RESET_FSM(); return fsm_rt_cpl; } s_tState = SEND_CHAR; break; case SEND_CHAR: if (serial_out(*pchStr)) { pchStr++; s_tState = IS_END_OF_STRING; } break; }
          return fsm_rt_on_going;}
          不仔細(xì)看的小伙伴也許會撓撓后腦勺,說:“代碼很漂亮……但我也沒看出有啥問題啊”?

          不打緊,我們來看看這個狀態(tài)機(jī)時如何使用的:
          int main(void){    ...    while(true) {        static const char c_tDemoStr[] = {"Hello world!\r\n"};
          print_str(c_tDemoStr);    }}

          還沒看出問題么?


          好了,節(jié)目效果到了,我也不賣關(guān)子了,這一狀態(tài)機(jī)存在的問題如下:
          • pchStr是一個局部變量,它保存了狀態(tài)機(jī)函數(shù) print_str 被調(diào)用時用戶所傳遞的字符串首地址;

          • 該狀態(tài)機(jī)在執(zhí)行的過程中,不可避免的要多次出讓(Yield)處理器時間,以達(dá)到“非阻塞”的目的;

          • 由于pchStr是一個局部變量,它的生命周期在退出print_str函數(shù)后就結(jié)束了;而每次重新進(jìn)入print_str函數(shù),它的值都會被復(fù)位成“hello world\r\n”的起始地址。


          這里,pchStr本質(zhì)上是狀態(tài)機(jī)print_str的上下文,該狀態(tài)圖設(shè)計最大的問題就是未保存print_str的上下文,導(dǎo)致每次進(jìn)出狀態(tài)機(jī)函數(shù)都會重新刷新關(guān)鍵的狀態(tài)信息。

          既然問題清楚了,修改方式也迎刃而解,如下圖所示:


          也就是說,我們可以通過引入一個靜態(tài)變量 s_pchStr的方式來保存狀態(tài)機(jī)的關(guān)鍵上下文信息。對比圖片,可以注意到:修改后的圖在復(fù)位后的初始化階段(也就是start的行為部分)對靜態(tài)變量 s_pchStr做了一個初始化——用pchStr為其賦值。此后,圖中所有針對字符串的操作也都是使用 s_pchStr 來完成了。

          重新翻譯后的代碼如下:
          fsm_rt_t print_str(const char *pchStr){    static enum {        START = 0,        IS_END_OF_STRING,        SEND_CHAR,    } s_tState = START;    static const char *s_pchStr = NULL;
          switch (s_tState) { case START: s_pchStr = pchStr; s_tState = IS_END_OF_STRING; //break; //!< fall-through case IS_END_OF_STRING: if (*s_pchStr == '\0') { PRINT_STR_RESET_FSM(); return fsm_rt_cpl; } s_tState = SEND_CHAR;            //break;    //!< fall-through case SEND_CHAR: if (serial_out(*s_pchStr)) { pchStr++; s_tState = IS_END_OF_STRING; } break; }
          return fsm_rt_on_going;}

          【一系列似是而非的問題……】


          經(jīng)過上面的一連串操作,我們成功的排除了陷阱,獲得了一個能正常工作的狀態(tài)機(jī)。然而,眼尖的小伙伴還是能很快的發(fā)現(xiàn)這里的限制:

          • 狀態(tài)機(jī)print_str 使用了靜態(tài)變量來保存狀態(tài)(s_tState)和關(guān)鍵的上下文(s_pchStr),因此幾乎肯定是不可重入的;

          • 狀態(tài)機(jī)print_str使用了共享函數(shù)serial_out(),即便該函數(shù)本身可以保證原子性,但它仍然是一個臨界資源——換句話說,即便拋開 print_str 的可重入性問題不談,當(dāng)有該狀態(tài)機(jī)存在多個實例時,你能保證每個字符串的打印都是完整的么?比如:

          int main(void){    ...    while(true) {        print_str(“I have a pen...”);        print_str("I have an apple...");    }}
          你實際打印出來的絕對不是你想要的結(jié)果。
          此時,我們可以說,print_str 也不是線程安全(thread-safe)的。

          根據(jù)維基百科的描述:

          In computing, ... a reentrant procedure can be interrupted in the middle of its execution and then safely be called again ("re-entered") before its previous invocations complete execution.

          https://en.wikipedia.org/wiki/Reentrancy_(computing)

          大體翻譯成中文就是:

          ……可重入的程序(函數(shù))允許在執(zhí)行的過程中被打斷,并在打斷所執(zhí)行的代碼中再次安全的調(diào)用……

          這里,我們需要注意一個細(xì)節(jié),就是“可重入”關(guān)注的是,在任意時刻,無論以什么樣的方式,該函數(shù)被多次調(diào)用時是否“安全”。換句話說,它并不是“非常在意”可重入本身對功能的影響,它在意的是這樣調(diào)用是否“安全”

          以我們的print_str為例,由于狀態(tài)機(jī)的中使用了靜態(tài)變量,尤其是狀態(tài)變量s_tState——這意味著同時執(zhí)行的多個實例,彼此共享同一個狀態(tài)變量……換句話說,當(dāng)多個print_str同時執(zhí)行時,它們是彼此干擾的。這意味著同時執(zhí)行多個print_str是“不安全”的,是會出問題的(比如字符串長度不一致時很可能會出現(xiàn)buffer-overflow的問題),因此可以說 print_str 是不可重入的。
          但換一個角度,假設(shè)我們已經(jīng)解決了print_str的不可重入問題,比如:妥善的解決了狀態(tài)變量和上下文的存儲問題,那么就滿足了“可重入”關(guān)于“安全”的要求——因為當(dāng)存在多個實例的時候,這樣執(zhí)行并不會導(dǎo)致系統(tǒng)崩潰,或是buffer-overflow——只不過打印出來的字符串并不完整而已。這就是為什么人們常說的:

          可重入的函數(shù)不一定線程安全;

          線程安全的函數(shù)也不一定可重入。


          本質(zhì)上,我們要解決的并不單純是狀態(tài)機(jī)的“可重入”問題——只把眼光放在可重入上就“格局小了”。


          我們要實現(xiàn)的是“支持多實例的狀態(tài)機(jī)”。

          【多實例的狀態(tài)機(jī)】


          所謂多實例的狀態(tài)機(jī),就是指那些同一時刻可以安全存在多個運(yùn)行實例的狀態(tài)機(jī)——本質(zhì)上每個實例都是一個任務(wù)——以多任務(wù)的眼光去看待狀態(tài)機(jī)的多實例問題,格局就寬闊了起來。

          通過前面的分析,我們已經(jīng)注意到了問題所在,即:以現(xiàn)有的實現(xiàn)方式,如果存在多個 print_str 調(diào)用(實例),那么它們其實是在“競爭”關(guān)鍵的狀態(tài)變量 s_tState和上下文 s_pchStr

          聰明的你一定看出來了,解決狀態(tài)機(jī)多實例的方式就是“給每個實例都發(fā)一個球”。具體來說,就是:

          • 為狀態(tài)機(jī)定義一個控制塊;

          • 在控制塊里存放狀態(tài)變量;

          • 在控制塊里存放狀態(tài)機(jī)的上下文;

          • 建立狀態(tài)機(jī)實例時,首先要建立一個控制塊,并對其進(jìn)行必要的初始化;

          • 在隨后調(diào)用狀態(tài)機(jī)時,應(yīng)該首先傳遞狀態(tài)機(jī)的控制塊給狀態(tài)機(jī)函數(shù)。


          對應(yīng)到圖例上,我們一般會在狀態(tài)圖的某個角落(比如左下角或右下角)通過一個矩形框列舉狀態(tài)機(jī)上下文的所有內(nèi)容。如下圖所示:


          觀察修改后的狀態(tài)圖,我們應(yīng)該注意以下的一些變化:

          • 在圖的右下角,出現(xiàn)了一個帶標(biāo)題的矩形框。這里標(biāo)題print_str_t是狀態(tài)機(jī)控制塊的類型名稱;下面的列表中列舉了上下文的內(nèi)容,在本例中就是 pchStr,注意,它已經(jīng)去掉了"s_"前綴。

          • 狀態(tài)圖中通過 "this.xxxx" 的方式來訪問狀態(tài)機(jī)上下文中的內(nèi)容。


          【基本的翻譯方法】


          一般來說,無論采用何種狀態(tài)機(jī)翻譯方式,可重入的狀態(tài)機(jī)一定會包含一個控制塊。在C語言中,我們會為其定義一個結(jié)構(gòu)體類型:
          typedef struct <控制塊類型名稱> {    uint8_t chState;      //!< 狀態(tài)變量    <上下文列表>} <控制塊類型名稱>;

          以print_str狀態(tài)圖為例:

          typedef struct print_str_t {    uint8_t chState;     //!< 狀態(tài)變量    const char *pchStr;  //!< 上下文} print_str_t;


          這里,我們并不會規(guī)定用戶用何種方式來為 print_str_t 類型分配存儲空間——這個選擇權(quán)應(yīng)該留個用戶自己——無論是定義靜態(tài)局部變量、全局變量還是從堆或者池中分配,都可以。
          無論采用哪種分配方式,我們都需要提供一個專門的函數(shù)來對狀態(tài)機(jī)進(jìn)行初始化。推薦的格式是:

          #undef this#define this    (*ptThis)...
          int <狀態(tài)機(jī)名稱>_init(<狀態(tài)機(jī)類型名稱> *ptThis[, <形參列表>]){ ...    this.chState = 0;    //!< 復(fù)位狀態(tài)變量,這里固定用0    /*! \note 這里根據(jù)需要可以初始化那些只需要初始化一次的上下文     */       /*! \note 這里也可以對輸入的參數(shù)進(jìn)行有效性檢測,如果發(fā)現(xiàn)錯誤,     *!       就返回負(fù)數(shù)值。這里既可以自定義一套枚舉,也可以簡單     *!       返回 -1 了事。 */ return 0; //!< 如果一切順利返回0,表示正常}
          以 print_str為例:
          int print_str_init(print_str_t *ptThis){    if (NULL == ptThis) {        return -1;     //!< 是的,我偷懶了    }        this.chState = 0;    //在這個例子中,this.pchStr 更適合在運(yùn)行時刻由用戶指定。        return 0;}

          接下來,我們就需要對狀態(tài)機(jī)函數(shù)進(jìn)行小小的改造,其格式為:
          #include <assert.h>
          fsm_rt_t <狀態(tài)機(jī)名稱>(<狀態(tài)機(jī)類型名> *ptThis[, <形參列表>]){    //!< 這種事情就不適合在release版本的運(yùn)行時刻檢查    assert(NULL != ptThis);       enum {        START = 0,        <狀態(tài)列表>    }; ...     switch (this.chState) {     ...    }        return fsm_rt_on_going;}



          最后,該圖的翻譯為:

          #undef this#define this (*ptThis)
          #define PRINT_STR_RESET_FSM() \ do { this.State = START; } while(0)
          fsm_rt_t print_str(print_str_t *ptThis, const char *pchStr){ enum { START = 0, IS_END_OF_STRING, SEND_CHAR, };
          switch (this.chState) { case START: this.pchStr = pchStr; this.chState = IS_END_OF_STRING; //break; //!< fall-through case IS_END_OF_STRING: if (*(this.pchStr) == '\0') { PRINT_STR_RESET_FSM(); return fsm_rt_cpl; } this.chState = SEND_CHAR; //break; //!< fall-through case SEND_CHAR: if (serial_out(*(this.pchStr))) { this.pchStr++; this.chState = IS_END_OF_STRING; } break; }
          return fsm_rt_on_going;}

          此時,我們就可以“安全”的進(jìn)行多實例調(diào)用了:

          static print_str_t s_tPrintTaskA;static print_str_t s_tPrintTaskB;
          int main(void){ ... print_str_init(&s_tPrintTaskA); print_str_init(&s_tPrintTaskB);     while(true) {        print_str(&s_tPrintTaskA, “I have a pen...”);        print_str(&s_tPrintTaskB, "I have an apple..."); }}


          至此,我們就完成了狀態(tài)機(jī)print_str多實例的整個改造和部署過程。


          【說在后面的話】


          實際上,無論你的狀態(tài)機(jī)本來就只需要單實例還是考慮要支持多實例,至少在Arm架構(gòu)下,統(tǒng)一采用支持多實例的方式來設(shè)計其實在上下文的訪問效率上是更高的,這在文章《散裝 vs 批發(fā)誰效率高?變量訪問被ARM架構(gòu)安排的明明白白》有詳細(xì)介紹,這里就不再贅述。

          一旦習(xí)慣了使用多實例的方式來設(shè)計狀態(tài)圖,其實你就真正進(jìn)入了“多任務(wù)程序設(shè)計”的領(lǐng)域——無論你是使用RTOS還是裸機(jī),此時此刻,或多或少,都在同一個起跑線上了。
          前面的例子講解中,我們還遺留了一個所謂線程安全(Thread-safe)的問題沒有解決。實際上,在完成了狀態(tài)機(jī)的多實例化改造后,這一問題其實已經(jīng)完全不是狀態(tài)機(jī)設(shè)計的問題了——而是一個地地道道的普通多任務(wù)間同步和通信的問題(IPC問題)。

          如何在狀態(tài)機(jī)設(shè)計中體現(xiàn)多任務(wù)通信的方法和設(shè)計原則,這是我們后續(xù)文章的課題。有興趣的小伙伴可以持續(xù)關(guān)注這個系列。

          從另外一個角度來看。我們?yōu)槊恳粋€狀態(tài)機(jī)都引入了一個控制塊,從面向?qū)ο箝_發(fā)的視角來看,本質(zhì)上是將狀態(tài)機(jī)都以類的形式進(jìn)行了改造,這里:

          • 控制塊的定義就是狀態(tài)機(jī)的類(Class)定義;

          • 狀態(tài)機(jī)函數(shù)是類的方法(Method);

          • 初始化函數(shù)是類的構(gòu)造函數(shù)(Constructor);

          • 實際上,狀態(tài)機(jī)函數(shù)中用 this 來訪問上下文,也已經(jīng)暴露其OO的本質(zhì)。


          結(jié)合我在《真刀真槍模塊化(2.5)—— 君子協(xié)定》介紹的方法,我們還可以真正做到對狀態(tài)機(jī)的類進(jìn)行私有化保護(hù)——是不是格局越來越大了呢?

          ????????????????  END  ????????????????

          關(guān)注我的微信公眾號,回復(fù)“加群”按規(guī)則加入技術(shù)交流群。

          歡迎關(guān)注我的視頻號:


          點(diǎn)擊“閱讀原文”查看更多分享,歡迎點(diǎn)分享、收藏、點(diǎn)贊、在看。

          瀏覽 67
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <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>
                  麻豆视频md0117 | 黄色成人在线免费观看 | AA级亚洲电影 | 欧美一级a视频免费放 | 爱爱网站无码 |