美文网首页单片机学习程序员
嵌入式开发系列教程(六) 并发模型之协程

嵌入式开发系列教程(六) 并发模型之协程

作者: qianlihu | 来源:发表于2017-04-26 16:39 被阅读111次
    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 语言协程库

    这是一个免费,开源的教程,如果你喜欢可以转发,也可以打赏奖励。 欢迎关注微信公众号小站练兵

    相关文章

      网友评论

        本文标题:嵌入式开发系列教程(六) 并发模型之协程

        本文链接:https://www.haomeiwen.com/subject/kxzizttx.html