嵌入式开发系列教程(六) 并发模型之协程
2017-04-26 本文已影响111人
qianlihu
void LedFlash()
{
LedOn();
delay(3); //点亮3s 我们希望此刻CPU能够处理其他任务,而不是忙等待
LedOff();
delay(5); //熄灭5s 我们希望此刻CPU能够处理其他任务,而不是忙等待
LedOn();
delay(5); //点亮5s 我们希望此刻CPU能够处理其他任务,而不是忙等待
LedOff();
delay(3); //熄灭3s 我们希望此刻CPU能够处理其他任务,而不是忙等待
}
我们希望在执行delay函数的时候,CPU能够处理其他任务,而不是忙等待,如果有OS的协助的话,这很容易实现,没有OS的话,我们前面用例回调函数+状态机的方法实现了上述功能,这里我们再介绍协程的方法,协程的概念其实很早就提出了。只不过最近由于并发编程的兴起,被更多的人关注,又火起来了。
在嵌入式领域,Contiki就借助了协程的概念。
协程模型
我们先来看一下回调模型中的写法
- 先点亮3s, 第一次闪烁FLASH=first,当前LED灯是熄灭状态LED=OFF STATE = 0x10
- 再熄灭5s, 第一次闪烁FLASH=first,当前LED灯是点亮状态LED=ON STATE = 0x11
- 然后点亮5s, 第二次闪烁FLASH=second,当前LED灯是熄灭状态LED=OFF STATE = 0x20
- 再熄灭3s 第二次闪烁FLASH=sencond,当前LED灯是点亮状态LED=ON STATE = 0x21
static int STATE = 0x10;
void LedFlash()
{
switch(STATE){
case 0x10:
LED = ON; //点亮LED灯
STATE = 0x11; //切换状态
setTimerEvent(3000,LedFlash);
break;
case 0x11:
LED = OFF;
STATE = 0x20; //切换状态
setTimerEvent(5000,LedFlash);
break;
case 0x20:
LED = ON;
STATE = 0x21; //切换状态
setTimerEvent(5000,LedFlash);
break;
case 0x21:
LED = OFF;
STATE = 0x10; //切换状态
setTimerEvent(3000,LedFlash);
break;
}
}
对于协程模型,我们希望看到这样的代码。
void LedFlash()
{
LedOn();
setTimerEvent(3000,LedFlash); //定时3s,3s后定时器系统再调用LedFlash函数
return; //函数返回,,这样的话,便不占用CPU资源
LedOff(); //3s后,我们希望LedFlash函数直接执行这一行
setTimerEvent(5000,LedFlash);
return;
LedOn();
setTimerEvent(5000,LedFlash);
return;
LedOff();
setTimerEvent(3000,LedFlash);
return;
}
很明显,C语言的return语义不能帮我们做到这一点,我们引入一个关键字yield,yield的功能便是使得函数返回,并且再下一次调用中,从该行的下一行开始执行代码。
void LedFlash()
{
LedOn();
setTimerEvent(3000,LedFlash); //定时3s,3s后定时器系统再调用LedFlash函数
yield; //yield,让出CPU
LedOff(); //3s后,我们希望LedFlash函数直接执行这一行
setTimerEvent(5000,LedFlash);
yield; //yield,让出CPU
LedOn();
setTimerEvent(5000,LedFlash);
yield; //yield,让出CPU
LedOff();
setTimerEvent(3000,LedFlash);
yield; //yield,让出CPU
}
那怎么实现这种功能呢?
void LedFlash()
{
static int state = 0; //注意 state 为什么要用static 修饰
switch(state){
case 0:
LedOn();
setTimerEvent(3000,LedFlash); //定时3s,3s后定时器系统再调用LedFlash函数
state = 1;
break;
case 1:
LedOff(); //3s后,我们希望LedFlash函数直接执行这一行
setTimerEvent(5000,LedFlash);
state = 2;
break;
case 2:
LedOn();
setTimerEvent(5000,LedFlash);
state = 3;
break;
case 3:
LedOff();
setTimerEvent(3000,LedFlash);
state = 4; //注意此处有bug,可以暂时忽略
break;
}
}
现在我们发现此处代码其实和回调模型中的代码逻辑几乎是一样的。那这跟协程,跟yield有什么关系呢?
在上述代码中,state状态被标记为[0,1,2,3]
了,其实state只要是四个不同的整数来代表不同的状态就行。 我们修改一下代码
void LedFlash()
{
static int state = 0; //注意 state 为什么要用static 修饰
switch(state){
case 0:
LedOn();
setTimerEvent(3000,LedFlash); //定时3s,3s后定时器系统再调用LedFlash函数
state = __LINE__ + 2; //state 状态切换为下个case的入口
return;
case __LINE__: //此处__LINE__ = 两行前的 __LINE__ + 2
LedOff(); //3s后,我们希望LedFlash函数直接执行这一行
setTimerEvent(5000,LedFlash);
state = __LINE__ + 2; //state 状态切换为下个case的入口
return;
case __LINE__: //此处__LINE__ = 两行前的 __LINE__ + 2
LedOn();
setTimerEvent(5000,LedFlash);
state = __LINE__ + 2; //state 状态切换为下个case的入口
return;
case __LINE__: //此处__LINE__ = 两行前的 __LINE__ + 2
LedOff();
setTimerEvent(3000,LedFlash);
state = __LINE__ + 2; //此处bug请自动忽略
return;
}
}
现在大家肯定发现了代码中有很多重复的地方,我们提炼一下
void LedFlash()
{
static int state = 0;
switch(state){
case 0: 参考 下文中 Begin()宏
; // 执行一条空语句,无意义 其实是Begin()后面的分号
LedOn();
setTimerEvent(3000,LedFlash);
state = __LINE__ ; reutrn; case __LINE__: //参考下文中Yield()宏
; // 执行一条空语句,无意义 其实是Yield()后面的分号
LedOff();
setTimerEvent(5000,LedFlash);
state = __LINE__ ; reutrn; case __LINE__:
; // 执行一条空语句,无意义
LedOn();
setTimerEvent(5000,LedFlash);
state = __LINE__ ; reutrn; case __LINE__:
; // 执行一条空语句,无意义
LedOff();
setTimerEvent(3000,LedFlash);
//请忽略下面的逻辑
} //参考下文中 End()宏
}
在这里的代码中,我们把state=__LINE__
case __LINE__
放到了一行,这样便少了一次加法操作。
我们用宏提炼一下上述代码
#define Begin() static int state=0; switch(state) { case 0:
#define Yield() state=__LINE__; return; case __LINE__:
#define End() }
void LedFlash()
{
Begin();
LedOn();
setTimerEvent(3000,LedFlash); //定时3s,3s后定时器系统再调用LedFlash函数
Yield(); //yield,让出CPU,对应代码我们可以看到是return返回
LedOff(); //3s后,LedFlash函数会根据state直接执行这一行
setTimerEvent(5000,LedFlash);
Yield(); //yield,让出CPU
LedOn();
setTimerEvent(5000,LedFlash);
Yield(); //yield,让出CPU
LedOff();
setTimerEvent(3000,LedFlash);
Yield(); //yield,让出CPU
End();
}
其实对于Yield(),我们可以用一种更安全的宏包装。
#define Yield() do { state=__LINE__; return; case __LINE__:; } while (0)
现在对比回调模型中的代码,我们发现,代码逻辑比较清晰了。
缺陷
这是一个非常简单的协程模型,其实就是Contiki的作者写的。Contiki也是用的这个模型。有几个缺陷
- 因为switch-case结构不能嵌套,使用此模型便不能正常的使用switch-case结构
- 代码中不能使用局部变量,若使用必须加static修饰。原因参考 state为什么必须家static修饰
文章参考:
一个“蝇量级” C 语言协程库
这是一个免费,开源的教程,如果你喜欢可以转发,也可以打赏奖励。 欢迎关注微信公众号小站练兵