从零开始漫谈 | 多实例的状态机
共 9388字,需浏览 19分钟
·
2021-10-02 00:40
来源:裸机思维
作者: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-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;
}
【一系列似是而非的问题……】
状态机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-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;
}
此时,我们就可以“安全”的进行多实例调用了:
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 ‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧ 关注我的微信公众号,回复“加群”按规则加入技术交流群。
点击“阅读原文”查看更多分享,欢迎点分享、收藏、点赞、在看。