事件驱动框架(三)——pt协程

事件驱动框架(三)——pt协程

说明:

现在进入了框架整合的阶段,虽然当初的目标是以界面为主,但是项目中肯定还要加入一些其他操作。虽然可以沿用状态表的状态机方式来构建,但是为了整合整个框架,并且之前看过PT协程的东西,就想先介绍一下PT协程,顺便再思考一下可不可以加到现有的框架里来,作为状态机实现的另一种方式。

——————————————————————————————————————————

PT协程的介绍:

协程(coroutine)顾名思义就是“协作的例程”(co-operative routines)。跟具有操作系统概念的线程不一样,协程是在用户空间利用程序语言的语法语义就能实现逻辑上类似多任务的编程技巧。
协程(coroutine)顾名思义就是“协作的例程”(co-operative routines)。跟具有操作系统概念的线程不一样,协程是在用户空间利用程序语言的语法语义就能实现逻辑上类似多任务的编程技巧。实际上协程的概念比线程还要早,按照 Knuth 的说法“子例程是协程的特例”,一个子例程就是一次子函数调用,那么实际上协程就是类 函数一样的程序组件,你可以在一个线程里面轻松创建数十万个协程,就像数十万次函数调用一样。只不过子例程只有一个调用入口起始点,返回之后就结束了,而 协程入口既可以是起始点,又可以从上一个返回点继续执行,也就是说协程之间可以通过 yield 方式转移执行权,对称(symmetric)、平级地调用对方,而不是像例程那样上下级调用关系。当然 Knuth 的“特例”指的是协程也可以模拟例程那样实现上下级调用关系,这就叫非对称协程(asymmetric coroutines)。
这就引出了协程的概念,如果将每个协程的上下文(比如程序计数器)保存在其它地方而不是堆栈上,协程之间相互调用时,被调用的协程只要从堆栈以外的地方恢复上次出让点之前的上下文即可,这有点类似于 CPU 的上下文切换,遗憾的是似乎只有更底层的汇编语言才能做到这一点。

出处:http://developer.51cto.com/art/201402/428768.htm

解释一段:宏LINE 就是行号。LINE+2就是return后一行代码case的行号。

int function(void) { 
  static int i, state = 0; 
  switch (state) { 
    case 0: /* start of function */ 
    for (i = 0; i < 10; i++) { 
      state = __LINE__ + 2; /* so we will come back to "case __LINE__" */ 
      return i; 
      case __LINE__:; /* resume control straight after the return */ 
    } 
  } 
} 

这种协程实现方法有个使用上的局限,就是协程调度状态的保存依赖于 static 变量,而不是堆栈上的局部变量, 实际上也无法用局部变量(堆栈)来保存状态,这就使得代码不具备可重入性和多线程应用。后来作者补充了一种技巧,就是将局部变量包装成函数参数传入的一个 虚构的上下文结构体指针,然后用动态分配的堆来“模拟”堆栈,解决了线程可重入问题。

这种类似于状态机结构的另一种写法,相当于把switch-case放到function里面了。而返回值和函数参数传入就解决的是上下文的问题。如果参数多(上下文不只一个变量),也就是UML里的条件多??可以在函数里处理后再实现跳转。

while(1)
{
    ret = function(ret);
}

PT协程源码编程思路:

上面链接中对PT协程代码解释的很细了。
但为了看起来方便(便于回忆)还是整理一下思路
pt协程的开源C的代码量很小而且不依赖于其他库,并且代码量很小。

pt协程有两种实现形式:
1.switch-case
这种方式有个缺点。它把switch-case的结构分散在了几个宏中。如果调用顺序漏写都会造成编译错误。而且中间不能包括switch-case嵌套。
2.goto label

基于 GNU C 的调度“原语”。在 GNU C 下还有一种语法糖叫做标签指针,就是在一个 label 前面加 &&(不是地址的地址,是 GNU 自定义的符号),可以用 void 指针类型保存,然后 goto 跳转:

源码把它用LC系列的宏来表示。主要有这么四个基础部分,模板参照上面那个function的功能。

LC_INIT(s)——状态量的初始化
LC_RESUME(s)——开始的地方——代码第一次要执行的地方
LC_SET(s)——跳转到的地方—— 代码要跳到执行的地方,可理解为一个状态(!!!)
LC_END(s)——结束,这个goto的那种是不需要的,switch-case用来表示语法完整的}

接着用PT系列的宏来吧LC宏进一步封装,同时宏里还隐藏一个PT_YIELD_FLAG变量来控制走向。开发就直接调用这些宏。这里的传入参数pt是经过包装的结构体= =
PT_INIT(pt)——直接调用LC的初始化
PT_THREAD(name_args)——
PT_BEGIN(pt)——声明PT_YIELD_FLAG变量并且调用LC跳转到初始的位置
PT_END(pt)——结束,调用LC的结束,标志位复位,调用LC初始化,返回PT_ENDED

PT_YIELD(pt)——协程让出——让出标志位复位,调用LC_SET跳转,判断标志位结束返回 PT_YIELDED。
PT_YIELD_UNTIL(pt, cond)——协程让出附加条件——让出标志位复位,调用LC_SET跳转,判断标志位并且条件成立结束返回 PT_YIELDED。
PT_WAIT_UNTIL(pt, condition)——调用LC_SET,判断条件不发生返回PT_WAITING。
PT_SEM_WAIT(pt, s) PT_SEM_SIGNAL(pt, s)——信号量实现

#define PT_SEM_WAIT(pt, s)  \ 
  do {            \ 
    PT_WAIT_UNTIL(pt, (s)->count > 0);    \ 
    --(s)->count;       \ 
  } while(0) 
#define PT_SEM_SIGNAL(pt, s) ++(s)->count

下面是源码自带的例子举例

#define NUM_ITEMS 32
#define BUFSIZE 8

static int buffer[BUFSIZE];
static int bufptr;

static void add_to_buffer(int item)      //添加到缓冲区
{
  printf("Item %d added to buffer at place %d\n", item, bufptr);  
  buffer[bufptr] = item;
  bufptr = (bufptr + 1) % BUFSIZE;
}
static int get_from_buffer(void)        //从缓冲区获得
{
  int item;
  item = buffer[bufptr];
  printf("Item %d retrieved from buffer at place %d\n", item, bufptr);
  bufptr = (bufptr + 1) % BUFSIZE;
  return item;
}

static int produce_item(void)        //打印生产ITEM
{
  static int item = 0;
  printf("Item %d produced\n", item);
  return item++;
}

static void consume_item(int item)    //打印消费ITEM
{
  printf("Item %d consumed\n", item);
}

static struct pt_sem full, empty;     //定义信号量full,empty

static PT_THREAD(producer(struct pt *pt))     //生产线程
{
  static int produced;

  PT_BEGIN(pt);

  for(produced = 0; produced < NUM_ITEMS; ++produced) {

    PT_SEM_WAIT(pt, &full);         //当信号量full为空时return(跳过下面不执行)

    add_to_buffer(produce_item()); //加入到缓冲

    PT_SEM_SIGNAL(pt, &empty);   //信号量empty自加

  }

  PT_END(pt);
}

static PT_THREAD(consumer(struct pt *pt))     //消费线程
{
  static int consumed;

  PT_BEGIN(pt);

  for(consumed = 0; consumed < NUM_ITEMS; ++consumed) {

    PT_SEM_WAIT(pt, &empty);        //当信号量empty为空时return(跳过下面不执行)

    consume_item(get_from_buffer());     //从缓冲中取得ITEM并打印    

    PT_SEM_SIGNAL(pt, &full);    //信号量full自加
  }

  PT_END(pt);
}

static PT_THREAD(driver_thread(struct pt *pt))    //驱动线程
{
  static struct pt pt_producer, pt_consumer;

  PT_BEGIN(pt);

  PT_SEM_INIT(&empty, 0);              //初始化empty信号量
  PT_SEM_INIT(&full, BUFSIZE);        //初始化full信号量

  PT_INIT(&pt_producer);       //初始化生产消费线程
  PT_INIT(&pt_consumer);

  PT_WAIT_THREAD(pt, producer(&pt_producer) &     //等待两个线程都结束执行PT_END
             consumer(&pt_consumer));

  PT_END(pt);
}


int main(void)
{
  struct pt driver_pt;

  PT_INIT(&driver_pt);  //初始化驱动线程

  while(PT_SCHEDULE(driver_thread(&driver_pt))) {   //运行直到完成退出

    /*
     * When running this example on a multitasking system, we must
     * give other processes a chance to run too and therefore we call
     * usleep() resp. Sleep() here. On a dedicated embedded system,
     * we usually do not need to do this.
     */

     /*
     *当运行在多任务系统下时,必须给别的处理器运行的机会。因此我们需要调用usleep(),sleep()
     *当只用于嵌入系统(不是多任务系统),我们就不必使用这个
     */

#ifdef _WIN32
    Sleep(0);
#else
    usleep(10);
#endif
  }
  return 0;
}

忘记写怎么使用了。
大概就是一开始调用PT_INIT将该协程初始化。
再在while里运行PT_SCHEDULE调度。由于整个过程是轮询的,即使协程阻塞点,也不会阻塞等待一个地方。

谈谈这个和事件驱动的差别:

PT协程相当于整个while里都在轮询每个协程,一次循环会将下面所有的协程都轮个遍,再执行对应的操作。
事件驱动的话则像是创建了一个对象后执行完初始化就不动了。根据后面所知while里其实是轮询着事件队列,而并不是每一个对象。单独的对象会通过发送事件将事件加入事件队列。因此感觉事件驱动是对象是一个非常被动的地位,有事件才会触发,没事件就不执行。
再扯一点就是之前有提到过的问题。PT协程是把他作为一个轮询线程的整个框架写进去的。但就写单个线程来说。本质还是跳转的写法,但他实际使用起来还是顺序执行的方式,因此写起来还是比较方便的。如果要塞进事件驱动的框架里,是应该保留他的跳转的写法。
原来的框架提供2个接口一个是init,另一个是dispatch。初始化可以兼容,dispatch….感觉可以改的出来。暂时放置先。。。
如果改成Pt协程的方式主要还是为方便顺序式编程的写法。

  • 4
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值