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 让人可以忘却底层恶心的实现。