2

我正在阅读由瑞典 SICS 的 Adam Dunkels 开发的 Contiki OS 中的 protothread 实现的源代码。我真的很困惑它的实现和Simon Tatham所展示的协同程序想法之间的细微差别——也就是说,为什么状态变量在Adam 的 protothread实现中不必是静态的,而在 Simon 的论文中声明为静态的?

我们先来仔细看看西蒙的讨论。例如,如果能写一个函数说

int function(void) {
   int i;
   for(i=0; i<10; i++)
       return i; //actually won't work in C
}

并且连续十次调用函数返回数字 0 到 9。

这可以通过在此函数中使用以下宏来实现:

#define crBegin static int state=0; switch(state) { case 0:
#define crReturn(i,x) do { state=__LINE__; return x; \
case __LINE__:; } while (0)
#define crFinish }
int function(void) {
    static int i;
    crBegin;
    for (i = 0; i < 10; i++)
        crReturn(1, i);
    crFinish;
}

正如预期的那样,调用此函数十次将给出 0 到 9。

不幸的是,如果我们像这样使用 Adam 的本地延续宏封装的 switch-case(Contiki src 树中的/core/sys/lc-switch.h),这将不起作用,即使您将状态变量设置为静态:

typedef unsigned short lc_t;
#define LC_INIT(s) s = 0; // the ";" must be a mistake...
#define LC_RESUME(s) switch(s) { case 0:
#define LC_SET(s) s = __LINE__; case __LINE__:
#define LC_END(s) }
int function(void) {
    static int i;
    lc_t s;
    LC_INIT(s);
    LC_RESUME(s);
    for (i = 0; i < 10; i++)
    {    return i;
         LC_SET(s);
    }
    LC_END(s);
}

在这里,就像 Simon 的示例一样,s 用作状态变量,它保留由 LC_SET(s) 设置的位置(屈服点)。并且当函数稍后恢复执行时(从头开始),它会根据 s 的值进行切换。这种行为产生的效果是函数在先前调用设置的屈服位置之后继续运行。

这两组宏的区别在于:

  1. 状态变量 s 在 Simon 的示例中是静态的,但在 Adam 的 LC 定义中是非静态的;
  2. crReturn 在返回结果之前设置状态,而在 Adam 的 LC 定义中,LC_SET(s) 纯粹设置状态并标记屈服点。

当然,后者不适用于函数中的这种 for 循环情况。这种“返回并继续”行为的关键在于状态变量是静态的以及在返回语句之前设置的状态。显然 LC 宏不满足任何要求。那么为什么 LC 宏是这样设计的呢?

我现在只能推测这些 LC 宏只是非常低级的原语,不应该以这个 for 循环示例中所示的方式使用。我们需要进一步构建那些包裹这些 LC 原语的 PT 宏,以使它们真正有用。并且 crReturn 宏仅用于演示目的,以特别适合 for 循环情况,因为并非每次您都想通过从函数返回来产生执行。

4

2 回答 2

1

正如您猜对的那样,所有应该在 coroute 返回之间保存其值的函数局部变量应该是静态的,此外,lc_t描述 couroutine 的当前状态的类型变量应该是静态的。要修复您的示例,请statics.

另一件事是你想返回一个值。Contiki protothreads 不支持返回任意值;它们只是描述线程是否仍处于活动状态或已经完成(PT_WAITING、、PT_YIELDED和状态)PT_EXITED的代码。PT_ENDED

LC_xxx但是,您可以使用宏轻松完成这项工作;您还需要一个标志(这个想法与 中的相同PT_YIELD()):

int function(void) {
    static int i;
    static lc_t s;
    int flag = 0; // not static!
    LC_INIT(s);
    LC_RESUME(s);
    for (i = 0; i < 10; i++) {
        flag = 1;
        LC_SET(s);
        if (flag) { /* don't return if came to this point straight from the invocation of the coroutine `function` */
          return i;
        }
    }
    LC_END(s);
}

Contiki protothread 库使用这些LC_xxx宏来实现PT_xxx宏,这些宏又用于创建对处理的应用程序级别(PROCESS_xxx宏)的支持。

lc_t状态变量实际上与 protothread 的状态相同:在https://github.com/contiki-os/contiki/blob/master/core/sys/pt.hpt结构简单定义为:

struct pt {
  lc_t lc;
};

pt结构又作为结构中的成员包含在内process(参见https://github.com/contiki-os/contiki/blob/master/core/sys/process.h)。Contiki 中的进程结构是全局变量,因此原线程状态存储在原线程协程的不同调用中。

大多数 couroutine-local 变量也需要是静态的这一事实通常(在研究论文中)被描述为这种编程模型的主要限制之一,但实际上这并不是什么大问题。

于 2016-04-17T11:28:53.873 回答
0

状态变量确实需要位于静态分配的内存中,其中包括您链接到的 Dunkels 示例中的全局变量。如果它是一个自动变量(在堆栈上,不是静态的),它的值将在函数的一次调用到下一次调用中丢失,除了在最琐碎的程序中。

使用lc-switch实现时,可以将Tatham的修改crReturn()成一个LC_SET_AND_RETURN()宏,为需要返回值的函数添加返回功能,如下图,只调用LC_SET(s); return;forvoid函数即可。

#include "lc.h"
#define LC_SET_AND_RETURN(lc, retval) do { lc = __LINE__ ; return retval; case __LINE__: } while (0)

int function(void) {
    static int i;
    static lc_t s;
    LC_RESUME(s);
    for (i = 0; i < 10; i++) {
        LC_SET_AND_RETURN(s, i);
    }
    return -1; // done
    LC_END(s);
}

LC_INIT()看起来它应该被称为 like LC_INIT(static lc_t s);。编码

static lc_t s;
LC_INIT(s);

扩展到

static lc_t s;
s = 0;;

这不等同于static lc_t s = 0;, 并导致代码以意想不到的方式运行。

您可以使用static lc_t LC_INIT(s);to 展开,static lc_t s = 0;;但这看起来很有趣。

于 2017-12-11T23:45:11.747 回答