从零开始漫谈 | 多实例的状态机
来源:裸机思维
作者:GorgonMeducer
【说在前面的话】

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);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;}
int main(void){...while(true) {static const char c_tDemoStr[] = {"Hello world!\r\n"};print_str(c_tDemoStr);}}
还没看出问题么?
pchStr是一个局部变量,它保存了状态机函数 print_str 被调用时用户所传递的字符串首地址;
该状态机在执行的过程中,不可避免的要多次出让(Yield)处理器时间,以达到“非阻塞”的目的;
由于pchStr是一个局部变量,它的生命周期在退出print_str函数后就结束了;而每次重新进入print_str函数,它的值都会被复位成“hello world\r\n”的起始地址。

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-throughcase IS_END_OF_STRING:if (*s_pchStr == '\0') {PRINT_STR_RESET_FSM();return fsm_rt_cpl;}s_tState = SEND_CHAR;//break; //!< fall-throughcase SEND_CHAR:if (serial_out(*s_pchStr)) {pchStr++;s_tState = IS_END_OF_STRING;}break;}return fsm_rt_on_going;}
【一系列似是而非的问题……】
状态机print_str 使用了静态变量来保存状态(s_tState)和关键的上下文(s_pchStr),因此几乎肯定是不可重入的;
状态机print_str使用了共享函数serial_out(),即便该函数本身可以保证原子性,但它仍然是一个临界资源——换句话说,即便抛开 print_str 的可重入性问题不谈,当有该状态机存在多个实例时,你能保证每个字符串的打印都是完整的么?比如:
int main(void){...while(true) {print_str(“I have a pen...”);print_str("I have an apple...");}}
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)
大体翻译成中文就是:
可重入的函数不一定线程安全;
线程安全的函数也不一定可重入。
【多实例的状态机】
为状态机定义一个控制块;
在控制块里存放状态变量;
在控制块里存放状态机的上下文;
建立状态机实例时,首先要建立一个控制块,并对其进行必要的初始化;
在随后调用状态机时,应该首先传递状态机的控制块给状态机函数。

在图的右下角,出现了一个带标题的矩形框。这里标题print_str_t是状态机控制块的类型名称;下面的列表中列举了上下文的内容,在本例中就是 pchStr,注意,它已经去掉了"s_"前缀。
状态图中通过 "this.xxxx" 的方式来访问状态机上下文中的内容。
【基本的翻译方法】
typedef struct <控制块类型名称> {uint8_t chState; //!< 状态变量<上下文列表>} <控制块类型名称>;
以print_str状态图为例:
typedef struct print_str_t {uint8_t chState; //!< 状态变量const char *pchStr; //!< 上下文} print_str_t;
...int <状态机名称>_init(<状态机类型名称> *ptThis[, <形参列表>]){...this.chState = 0; //!< 复位状态变量,这里固定用0/*! \note 这里根据需要可以初始化那些只需要初始化一次的上下文*//*! \note 这里也可以对输入的参数进行有效性检测,如果发现错误,*! 就返回负数值。这里既可以自定义一套枚举,也可以简单*! 返回 -1 了事。*/return 0; //!< 如果一切顺利返回0,表示正常}
int print_str_init(print_str_t *ptThis){if (NULL == ptThis) {return -1; //!< 是的,我偷懒了}this.chState = 0;//在这个例子中,this.pchStr 更适合在运行时刻由用户指定。return 0;}
fsm_rt_t <状态机名称>(<状态机类型名> *ptThis[, <形参列表>]){//!< 这种事情就不适合在release版本的运行时刻检查assert(NULL != ptThis);enum {START = 0,<状态列表>};...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-throughcase IS_END_OF_STRING:if (*(this.pchStr) == '\0') {PRINT_STR_RESET_FSM();return fsm_rt_cpl;}this.chState = SEND_CHAR;//break; //!< fall-throughcase SEND_CHAR:if (serial_out(*(this.pchStr))) {this.pchStr++;this.chState = IS_END_OF_STRING;}break;}return fsm_rt_on_going;}
此时,我们就可以“安全”的进行多实例调用了:
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...");}}
至此,我们就完成了状态机print_str多实例的整个改造和部署过程。
【说在后面的话】
控制块的定义就是状态机的类(Class)定义;
状态机函数是类的方法(Method);
初始化函数是类的构造函数(Constructor);
实际上,状态机函数中用 this 来访问上下文,也已经暴露其OO的本质。
结合我在《真刀真枪模块化(2.5)—— 君子协定》介绍的方法,我们还可以真正做到对状态机的类进行私有化保护——是不是格局越来越大了呢?
‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧ END ‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧ 关注我的微信公众号,回复“加群”按规则加入技术交流群。
点击“阅读原文”查看更多分享,欢迎点分享、收藏、点赞、在看。
