一个“蝇量级”C语言协程库

今天看到一篇文章,读完后收获了两种协程实现方法
原文:
一个“蝇量级”C语言协程库:http://developer.51cto.com/art/201402/428768.htm

协程(coroutine)顾名思义就是“协作的例程”(co-operative routines)。跟具有操作系统概念的线程不一样,协程是在用户空间利用程序语言的语法语义就能实现逻辑上类似多任务的编程技巧。实际上协程的概念比线程还要早,按照 Knuth 的说法“子例程是协程的特例”,一个子例程就是一次子函数调用,那么实际上协程就是类 函数一样的程序组件,你可以在一个线程里面轻松创建数十万个协程,就像数十万次函数调用一样。只不过子例程只有一个调用入口起始点,返回之后就结束了,而 协程入口既可以是起始点,又可以从上一个返回点继续执行,也就是说协程之间可以通过 yield 方式转移执行权,对称(symmetric)、平级地调用对方,而不是像例程那样上下级调用关系。当然 Knuth 的“特例”指的是协程也可以模拟例程那样实现上下级调用关系,这就叫非对称协程(asymmetric coroutines)。
C 语言的“yield 语义”
下面的教程来自于一位 ARM 工程师、天才黑客 Simon Tatham(开源 Telnet/SSH 客户端 PuTTY 和汇编器 NASM 的作者,吐槽一句,PuTTY的源码号称是所有正式项目里最难 hack 的 C,你应该猜到作者是什么语言出身)的博文:Coroutines in C。中文译文在这里。

我们知道 python 的 yield 语义功能类似于一种迭代生成器,函数会保留上次的调用状态,并在下次调用时会从上个返回点继续执行。用 C 语言来写就像这样:

int function(void) {
  int i;
  for (i = 0; i < 10; i++)
    return i;   /* won't work, but wouldn't it be nice */
}

连续对它调用 10 次,它能分别返回 0 到 9。该怎样实现呢?可以利用 goto 语句,如果我们在函数中加入一个状态变量,就可以这样实现:

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

这个方法是可行的。我们在所有需要 yield 的位置都加上标签:起始位置加一个,还有所有 return 语句之后都加一个。每个标签用数字编号,我们在状态变量中保存这个编号,这样就能在我们下次调用时告诉我们应该跳到哪个标签上。每次返回前,更新状态变 量,指向到正确的标签;不论调用多少次,针对状态变量的 switch 语句都能找到我们要跳转到的位置。

先来一个简单的栗子:

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 */
    }
  }
}

连续对它调用 10 次,它能分别返回 0 到 9。

1 switch case 跳转方式

#define LC_INIT(s) s = 0;  // 源码中是有分号的,一个低级 bug,啊哈~
#define LC_RESUME(s) switch (s) { case 0:
#define LC_SET(s) s = __LINE__; case __LINE__:
#define LC_END(s) }

但这种“原语”有个难以察觉的缺陷:就是你无法在 LC_RESUME 和 LC_END (或者包含它们的组件)之间的代码中使用 switch-case语句,因为这会引起外围的 switch 跳转错误!为 此,protothreads 又实现了基于 GNU C 的调度“原语”。在 GNU C 下还有一种语法糖叫做标签指针,就是在一个 label 前面加 &&(不是地址的地址,是 GNU 自定义的符号),可以用 void 指针类型保存,然后 goto 跳转:
2 goto 跳转方式

typedef void * lc_t;
#define LC_INIT(s) s = NULL
#define LC_RESUME(s) \
  do { \
    if (s != NULL) { \
      goto *s; \
    }
  } while (0)
#define LC_CONCAT2(s1, s2) s1##s2
#define LC_CONCAT(s1, s2) LC_CONCAT2(s1, s2)
#define LC_SET(s) \
  do { \
    LC_CONCAT(LC_LABEL, __LINE__): \
    (s) = &&LC_CONCAT(LC_LABEL, __LINE__); \
  } while (0)

转载请注明:小Y » 一个“蝇量级”C语言协程库

赞 (3) 评论 (0) 分享 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址