ProtoThread原理及应用

1. 概述

1.1. 概念

进程,一个具有独立功能的正在运行的程序实例。进程是相互独立的,并且可以同时运行。
线程,描述一段代码的执行路径。线程属于进程,每个进程至少有一个线程。线程有自己独立的栈,多个线程可以同时运行。
协程,coroutine,可以看作co-routine,也即协作程序。几个程序协作运行,可以理解为轻量级线程。

1.2. 应用

进程和线程都是重量级的,功能更强大,但是开销同样更大。尤其是针对一些嵌入式设备,受限于空间和性能,无法使用进程和线程。此时有一些并发需要,如果沿用通常的顺序流程来开发,代码的逻辑处理会比较复杂。面对这种情况,需要有一个类似并发的代码开发模型,协程就是一个简单的并发开发模型。

1.3. ProtoThread

ProtoThread是一个极简的C语言协程库,由5个简单的.h文件构成。ProtoThread主要是利用switch case内嵌循环的特殊语法来实现的。因为ProtoThread完全是利用C语言的语法特性,所以ProtoThread可以适用所有C/C++项目。

2. 原理

2.1. 达夫设备

达夫设备(Duff’s device)是串行复制(serial copy)的一种优化实现,实现展开循环,进而提高执行效率。达夫设备的提出,为后面协程代码提供了基础条件。

void send( int * to, int * from, int count)
{
    int n = (count + 7 ) / 8 ;
    switch (count % 8 ) {
    case 0 :    do { * to ++ = * from ++ ;
    case 7 :          * to ++ = * from ++ ;
    case 6 :          * to ++ = * from ++ ;
    case 5 :          * to ++ = * from ++ ;
    case 4 :          * to ++ = * from ++ ;
    case 3 :          * to ++ = * from ++ ;
    case 2 :          * to ++ = * from ++ ;
    case 1 :          * to ++ = * from ++ ;
           } while (-- n > 0);
    }
}

初看上述代码,感觉非常奇怪,这样的代码竟然能够执行。switch case在编译后,生成跳转表。

2.1. 协程

下面的代码是Python中协程的一个应用,发生器。每次调用都会自动从上一次的代码处执行。

def foo(num):
    while num < 10:
        num = num+1
        yield num

我们可以用goto来模拟类似的效果。看下面代码:

int function(void) 
{
    static int i = 0;
    static int state = 0;
    if (0 == state) 
    {
        goto LABEL0;
    }
    else
    {
        goto LABEL1;
    }
LABEL0: 
    for (i = 0; i < 10; i++) 
    {
        state = 1; 
        return i;
LABEL1:; 
    }
    return 0;
}
int main()
{
    for (int i = 0; i < 10; i++)
  {
        function();
  }
    return 0;
}

function()在第一次调用时,跳转到LABLE0:;第二次调用时直接跳转到LABLE1:,再往下执行则到了for循环自加并进行判断比较。goto和switch case都是跳转表,可以互换。

int function1(void) 
{
    static int i = 0;
    static int state = 0;
    switch (state) 
    {
    case 0: 
        for (i = 0; i < 10; i++) 
        {
            state = 1; 
            return i;
    case 1:; 
        }
    }
    return 0;
}

我们将function1按Python中的协程形式进行修改:
下面使用了编译宏__LINE__,可以让case更具唯一性,减少命名的负担。do while(0)只是用来保证宏的安全性。每次调用function3时,即从YIELD(i)处退出,下次调用即进入for自加并判断。
为什么要用宏来将代码进行一个修改?上述代码虽然语法正确,但是语法形式上非常的怪异,尤其是当逻辑代码更多时,非常难以理解。为了突出算法的逻辑,而优化语法的形式是值得的。

#define BEGIN() static int state=0; switch(state) { case 0:
#define YIELD(x) do { state=__LINE__; return x; case __LINE__:; } while (0)
#define END() }
int function3(void) 
{
    static int i = 0;
    BEGIN();
    for (i = 0; i < 10; i++)
    {
        YIELD(i);
    }
    END();
    return 0;
}
void main()
{
    // 下面两个function3函数,执行一次for循环即交由另外一个函数执行一次for循环
    // 表现上和真正的协程基本一样
    for (;;)
  {
        function3();
        function3();
  }
}

3. ProtoThread

上述的协程,基本功能比较简单。ProtoThread在此基础上,添加了一些等待跳转的功能。

3.1. 基本代码

struct pt {
  lc_t lc;
};
typedef unsigned short lc_t;
#define LC_INIT(s) s = 0;
#define LC_RESUME(s) switch(s) { case 0:
#define LC_SET(s) s = __LINE__; case __LINE__:
#define LC_END(s) }
#define PT_BEGIN(pt) { char PT_YIELD_FLAG = 1; LC_RESUME((pt)->lc)
#define PT_END(pt) LC_END((pt)->lc); PT_YIELD_FLAG = 0; \
                   PT_INIT(pt); return PT_ENDED; }

3.2. 示例一

下面的示例,两个function()函数每执行一次即跳到对方函数执行,两个函数的功能看起来是并行的。

#define PT_YIELD(pt)                \
  do {                      \
    PT_YIELD_FLAG = 0;              \
    LC_SET((pt)->lc);               \
    if(PT_YIELD_FLAG == 0) {            \
      return PT_YIELDED;            \
    }                       \
  } while(0)
void Function(void)
{
    static int i = 0;
    static struct pt ptFlag;
        // 代码每次执行到此处即跳转到PT_YIELD之后的代码
    PT_BEGIN(&ptFlag);
    for (i = 0; i < 10; i++)
    {
                // 每次执行时记录当前位置行号,并退出当前函数
        PT_YIELD(&ptFlag);
        printf("Function:%d\n", i);
    }
    PT_END(&ptFlag);
}
void main()
{
      int nIdx = 0;
    for (nIdx = 0; nIdx < 10; nIdx++)
    {
        Function();
        Function();
    }
}

3.3. 示例二

PT_YIELD最简单,直接让出当前代码的执行权限。PT_WAIT_UNTIL则需要根据条件语句来进行判断,直到条件不满足时,才会出让当前执行权限,功能上相较于PT_YIELD更灵活。

#define PT_WAIT_UNTIL(pt, condition)            \
  do {                      \
    LC_SET((pt)->lc);               \
    if(!(condition)) {              \
      return PT_WAITING;            \
    }                       \
  } while(0)
/**
 * This is a very small example that shows how to use
 * protothreads. The program consists of two protothreads that wait
 * for each other to toggle a variable.
 */
/* We must always include pt.h in our protothreads code. */
#include "pt.h"
#include  /* For printf(). */
/* Two flags that the two protothread functions use. */
static int protothread1_flag = 0;
static int protothread2_flag = 0;
/**
 * The first protothread function. A protothread function must always
 * return an integer, but must never explicitly return - returning is
 * performed inside the protothread statements.
 *
 * The protothread function is driven by the main loop further down in
 * the code.
 */
static int protothread1(struct pt *pt)
{
  /* A protothread function must begin with PT_BEGIN() which takes a
     pointer to a struct pt. */
  PT_BEGIN(pt);
  /* We loop forever here. */
  while (1)
  {
    /* Wait until the other protothread has set its flag. */
    PT_WAIT_UNTIL(pt, protothread2_flag != 0);
    printf("Protothread 1 running\n");
    /* We then reset the other protothread's flag, and set our own
       flag so that the other protothread can run. */
    protothread2_flag = 0;
    protothread1_flag = 1;
    /* And we loop. */
  }
  /* All protothread functions must end with PT_END() which takes a
     pointer to a struct pt. */
  PT_END(pt);
}
/**
 * The second protothread function. This is almost the same as the
 * first one.
 */
static int protothread2(struct pt *pt)
{
  PT_BEGIN(pt);
  while (1)
  {
    /* Let the other protothread run. */
    protothread2_flag = 1;
    /* Wait until the other protothread has set its flag. */
    PT_WAIT_UNTIL(pt, protothread1_flag != 0);
    printf("Protothread 2 running\n");
    /* We then reset the other protothread's flag. */
    protothread1_flag = 0;
    /* And we loop. */
  }
  PT_END(pt);
}
/**
 * Finally, we have the main loop. Here is where the protothreads are
 * initialized and scheduled. First, however, we define the
 * protothread state variables pt1 and pt2, which hold the state of
 * the two protothreads.
 */
static struct pt pt1, pt2;
int main(void)
{
  /* Initialize the protothread state variables with PT_INIT(). */
  PT_INIT(&pt1);
  PT_INIT(&pt2);
  /*
   * Then we schedule the two protothreads by repeatedly calling their
   * protothread functions and passing a pointer to the protothread
   * state variables as arguments.
   */
  while (1)
  {
    protothread1(&pt1);
    protothread2(&pt2);
  }
}

3.4. 示例三

PT_WAIT_UNTIL根据自定义条件判断是否出让执行权限,但是有时有这样的需求,生产者生产达到10件商品时,通知消费者去取。这种需求,需要有类似信号量的一种同步方法。ProtoThread提供了类似的机制,通过计数来判断是否出让执行权限。每等一次,计数减1,每设置一次信号,则记数加1。
示例是类型的生产者与消费者模型。

struct pt_sem {
  unsigned int count;
};
#define PT_SEM_INIT(s, c) (s)->count = c
#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
#ifdef _WIN32
#include 
#else
#include 
#endif
#include 
#include "pt-sem.h"
#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)
{
  static int item = 0;
  printf("Item %d produced\n", item);
  return item++;
}
static void consume_item(int item)
{
  printf("Item %d consumed\n", item);
}
static struct pt_sem full, empty;
static PT_THREAD(producer(struct pt *pt))
{
  static int produced;
  PT_BEGIN(pt);
  for (produced = 0; produced < NUM_ITEMS; ++produced)
  {
    // full描述等待次数,满了则retrun出去
    PT_SEM_WAIT(pt, &full);
    add_to_buffer(produce_item());
    // 标记信息就相当于记数
    PT_SEM_SIGNAL(pt, &empty);
  }
  PT_END(pt);
}
static PT_THREAD(consumer(struct pt *pt))
{
  static int consumed;
  PT_BEGIN(pt);
  for (consumed = 0; consumed < NUM_ITEMS; ++consumed)
  {
    // Producer记的数如果清空了则return
    PT_SEM_WAIT(pt, &empty);
    consume_item(get_from_buffer());
    PT_SEM_SIGNAL(pt, &full);
  }
  PT_END(pt);
}
static PT_THREAD(driver_thread(struct pt *pt))
{
  static struct pt pt_producer, pt_consumer;
  // PT_BEGIN第一次执行时会顺序执行,再次执行时,就会跳转到上次退出的位置
  PT_BEGIN(pt);
  PT_SEM_INIT(&empty, 0);
  PT_SEM_INIT(&full, BUFSIZE);
  PT_INIT(&pt_producer);
  PT_INIT(&pt_consumer);
  // pt只是用来构建switch case的结构
  // 以后直接跳转到PT_WAIT_THREAD后面的判断条件,判断是继续循环还是退出
  PT_WAIT_THREAD(pt, producer(&pt_producer) & 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.
     */
#ifdef _WIN32
    Sleep(0);
#else
    usleep(10);
#endif
  }
  return 0;
}

3.5. 优缺点

优点:

  1. 代码简单,占用空间小,适用于价廉的嵌入式设备。
  2. 可移植性好,因为完全基于C语法实现的,C51/ARM等平台均支持。

缺点:

  1. 因为是Stackless,所以无法保存并恢复栈变量,代码中的有需要保存状态的变量需要使用静态局部变量代替。
  2. 代码并不是真的协程,编写代码的逻辑需要特别小心。

你可能感兴趣的:(嵌入式,C-C++)