Protothreads - lightweight stackless threads

查看原文

本文介绍了 Protothread 这种无栈线程的小众实现,由于不需要线程或者多进程就能实现多任务协程,它被用于 Arduino / 感应器这种嵌入式设备中。每个 PT 仅占用两个 bytes 大小,理论上你可以创建比 Actor 模型还多两个数量级的并发任务出来。由于仅仅是几个 C 的宏,它极具迁移性,甚至可以在无 OS 的环境运行。它的核心实现就下面几行代码:

struct pt { unsigned short lc; };
#define PT_THREAD(name_args)  char name_args
#define PT_BEGIN(pt)          switch(pt->lc) { case 0:
#define PT_WAIT_UNTIL(pt, c)  pt->lc = __LINE__; case __LINE__: \
                              if(!(c)) return 0
#define PT_END(pt)            } pt->lc = 0; return 2
#define PT_INIT(pt)           pt->lc = 0

以下代码

static
PT_THREAD(example(struct pt *pt))
{
  PT_BEGIN(pt);

  while(1) {
    PT_WAIT_UNTIL(pt,
      counter == 1000);
    printf("Threshold reached\n");
    counter = 0;
  }

  PT_END(pt);
}

扩展后可变为

static
char example(struct pt *pt)
{
  switch(pt->lc) { case 0:

  while(1) {
    pt->lc = 12; case 12:
    if(!(counter == 1000)) return 0;
    printf("Threshold reached\n");
    counter = 0;
  }

  } pt->lc = 0; return 2;
}

其中 pt->lc = 12; case 12: 这行代码非常鬼畜,它包含在了 while(1) 里面,但是确实可以这么使用。

下面是 我用 zserge/pt 的 PT 版本实现的 ping-pong 模型(在 Erlang 里面常当作教程的例子):

#include <stdio.h>
#include <stdlib.h>
#include "pt.h"

typedef pt_queue(uint8_t, 1) byte_queue_t;

void ping(struct pt *pt, int *ok) {
    static uint8_t *mail;
    pt_begin(pt);
    for(;;) {
        pt_wait(pt, *ok);
        printf("ping\n");
        *ok = 0;
    }
    pt_end(pt);
}

void pong(struct pt *pt, int *ok) {
    static uint8_t *mail;
    pt_begin(pt);
    for(;;) {
        pt_wait(pt, !*ok);
        printf("pong\n");
        *ok = 1;
    }
    pt_end(pt);
}

int main(int argc, char *argv[]) {
    struct pt ping_pt = pt_init();
    struct pt pong_pt = pt_init();
    int x = 1;
    for(;;) {
        ping(&ping_pt, &x);
        pong(&pong_pt, &x);
    }
    return 0;
}

从原理上讲,Protothread 其实就是一个轻量的协程库,函数内部需要自己主动写一个切出控制权的语句(pt_wait),而这个语句唯一在意要存储的变量就是下次跳进来还从这里运行,所以这个通过行号 __LINE__ 实现的数字就成了整个 struct pt 唯一需要放进内存的值。

衍生思考:探究语言的边际 Case 可以催生很好玩的的使用场景。合理编排而又有美感的 API 让人可以忘却底层恶心的实现。