
“协程的本质是利用程序语言语法来实现逻辑上的多任务的编程;”
很多年前,我在小单片机上一直想跑操作系统,奈何Flash和RAM一直没有合适的;后来想自己怼个操作系统,结果拖延症犯了,到现在也无果(rtt,freertos真香);后来一直在想有啥更好的方式去写代码,想着程序也就是"时间+内存+状态",然后得到了状态机;
状态机对于单片机程序来说,基本就是一个状态来带动另个状态,在c语言中实现状态切换的基本就是switch了,下面是简单的实现;
while(1){
switch(我是状态){
case 状态1:
下个状态是2;
break;
case 状态2:
下个状态是4;
break;
case 状态3:
这里结束了;
break;
case 状态4:
下个状态是3;
break;
}
}
不同的状态执行不同的代码块,这时可以将时间也作为一个状态变量加入,这样就实现了一个"前后台+时间片轮转"的代码架构; 但是状态越多就会越复杂,比如我要延时10ms去运行A在延时500ms去运行B这个状态机实现就会麻烦很多:
//设定时器时1ms运行一次,运行回调时全局变量时基g_TickCount++;
int main(void)
{
int State = 0; //设State是状态,默认等于0;
int Time;
//状态机
while(1){
switch(State){
//准备10ms延时
case 0:
Time = g_TickCount+10; //要延时10ms,时基+10
State = 1; //下个状态
break;
//延时10ms
case 1:
if(g_TickCount >= Time){ //定时器在一直运行,g_TickCount在10ms后会超过Time,
State = 2; //这里状态一直不变,等于延时了10ms,时间到了则立刻切换;
}
break;
//执行A,并准备下个延时时间的数据
case 2:
Time = g_TickCount+500; //要延时500ms,时基+500
State = 3; //下个状态
break;
//延时500
case 3:
if(g_TickCount >= Time){ //定时器在一直运行,g_TickCount在500ms后会超过Time,
State = 4; //这里状态一直不变,等于延时了500ms,时间到了则立刻切换;
}
break;
//执行B
case 4:
break;
}
}
}
到这里大家可以看出,在while下的switch是可以当做独立的一个任务,若是我要在这个代码里多跑几个"延时10ms去运行A在延时500ms去运行B",那可以多放几个switch,每个switch任务相互独立,宏观上看就像多个任务同时运行;
协程终于终于到正题了, 协程就是优化了上面的状态机; 其实就是利用程序语言语法来实现逻辑上的多任务的编程; 网上有大神说"任何编程规范,坚持牺牲算法清晰度来换取语法清晰度的,都应该重写.",虽不明但觉厉;
用switch状态机最麻烦的是状态的定义,状态需要每个不同且需要手动去写,任务程序大了后状态量会越来越臃肿,程序结构也会越来越不清晰,那有啥办法解决这个问题?
在 ANSI C 中预定义了很多个宏,其中有一个宏"__LINE__“,替换后是当前文件下”__LINE__“所在的行数,这样就使”__LINE__“在每个文件中有了唯一性; 用”__LINE__"作为状态,则解决了状态的的唯一性; 剩下的就是要咋去解决手动定义下个状态的问题了,我们来看一段代码;
typedef unsigned short ushort; //快捷定义一个无符号16位的类型
int main(void)
{
static ushort BP1 = 0; //定义一个全局变量,作为任务1断点
static ushort BP2 = 0; //定义一个全局变量,作为任务2断点
//===
while(1){
//任务1
switch(BP1){
case 0:
BP1 = __LINE__; goto GOTO_End1; case __LINE__: //这里必须一行,这样BP指向和状态是同个行号
BP1 = __LINE__; goto GOTO_End1; case __LINE__: //这里必须一行,这样BP指向和状态是同个行号
BP1 = __LINE__; goto GOTO_End1; case __LINE__: //这里必须一行,这样BP指向和状态是同个行号
}
GOTO_End1: //这是一个goto跳转的标签(任务1)
//任务2
switch(BP2){
case 0:
BP2 = __LINE__; goto GOTO_End2; case __LINE__: //这里必须一行,这样BP指向和状态是同个行号
BP2 = __LINE__; goto GOTO_End2; case __LINE__: //这里必须一行,这样BP指向和状态是同个行号
BP2 = __LINE__; goto GOTO_End2; case __LINE__: //这里必须一行,这样BP指向和状态是同个行号
}
GOTO_End2: //这是一个goto跳转的标签(任务2)
}
}
上面代码用一个全局变量来保存当前"__LINE__“,然后goto跳出任务,等待下个周期调用任务又回到了”__LINE__"继续下个代码段处理,就像在代码中自动设置了可恢复状态的断点;
在代码中也可以看出设置断点跳出的代码都是相同,switch的代码也是相同的;这时候我们就可以更加简化的用宏定义封装代码:
//协程:任务的开始
#define _COR_Start(BP)
switch((BP)){
case 0:
//协程:任务的结束
#define _COR_End()
}
GOTO_End:;
//协程:设置一个断点并跳出
#define _COR_SetBPBreak(BP)
(BP) = __LINE__; goto GOTO_End; case __LINE__:
这样一个简单的协程雏形就出来了,我们优化下之前的代码,用协程宏来替代:
typedef unsigned short ushort; //快捷定义一个无符号16位的类型
//任务1
void Task1(void)
{
static ushort BP = 0; //定义一个全局变量,作为任务断点
//===
_COR_Start(BP); //任务开始
_COR_SetBPBreak(BP); //设置断点并跳出
_COR_SetBPBreak(BP); //设置断点并跳出
_COR_SetBPBreak(BP); //设置断点并跳出
_COR_End(BP); //任务结束
}
//任务2
void Task2(void)
{
static ushort BP = 0; //定义一个全局变量,作为任务断点
//===
_COR_Start(BP); //任务开始
_COR_SetBPBreak(BP); //设置断点并跳出
_COR_SetBPBreak(BP); //设置断点并跳出
_COR_SetBPBreak(BP); //设置断点并跳出
_COR_End(BP); //任务结束
}
int main(void)
{
while(1){
Task1(); //任务1
Task2(); //任务2
}
}
可以看到用协程代码替换后整个代码结构就清晰整齐了很多;
基础协程完成,后面就要处理时间了;
同时还有一个问题,switch是轮询跳转的,协程跳转多了后性能会降低,代码越执行到后面,性能越低;当然这个性能时间微乎其微完全可以忽略,但是追求极致的我们肯定还是要纠结一把;
未完待续