超轻量级线程库Protothreads引子

Protothreads是一个Stackless的超轻量级线程库,其特点是顺序执行异步事件驱动的代码(类比协程),可以用于取代复杂的状态机逻辑;由于连栈空间都省了,因此一般用于内存稀缺的系统如嵌入式或传感器网络,但其特点也适用于常见的异步网络框架中的业务代码编码。其核心思想比较简单,本文相关代码和宏定义为简化重写版,且仅涉及小部分.

一、Label变量
GCC编译器支持label变量,即可以将label的地址值保存在一个void*变量里:

void *jump_addr = &&label_A;  // "&&" 是gcc的扩展操作符
...
label_A:
...
goto *jump_addr  //调到其指向的label处执行

那么受其启发我们可以用这个变量来保存一个最小的上下文信息,使得单个可重入函数每次按需执行不同的任务,一个完整的例子如下:

void *context = NULL;
int a, b;

int loop_handler(int last)
{
    int c = 0;
    int d = 0;

    if (context != NULL)
        goto *context;

LABEL_start:
    a = 99;
    b = 1000;
    printf("Here we go, setting a = %d and b = %d\n", a, b);
    context = &&LABEL_do_add;  //返回前指定下一个动作,即"重入地址",下同
    return 0;

LABEL_do_add:
    c = a + b;
    printf("a + b == %d\n", c);
    context = &&LABEL_do_mul;
    return c;

LABEL_do_mul:
    d = a * b;
    printf("a * b == %d\n", d);
    context = &&LABEL_end;
    return d;

LABEL_end:
    printf("We ar done, bye bye\n");
    return 0;
}

int test1()
{
    int s = 0;
    for (int i = 0; i < 10; ++i)
    {
        s = loop_handler(s);
    }
    return 0;
}


运行test1()输出为:
Here we go, setting a = 99 and b = 1000
a + b == 1099
a * b == 99000
We are done, bye bye
We are done, bye bye
We are done, bye bye
We are done, bye bye
We are done, bye bye
We are done, bye bye
We are done, bye bye

这里实际上是把状态存在了context里,等价于状态机if-else或switch-case的功能。可以进一步将通用的部分抽取封装为简单的宏定义,使得代码看上去更整洁,并且不需要自己定义label:

#define _CONCAT(a, b) a ## b
#define CONCAT(a, b) _CONCAT(a, b)   //这里必须要多嵌套一层,否则下面的 __LINE__ 无法被展开
#define NEW_LABEL CONCAT(label, __LINE__)

#define BEGIN \
    do \
    { \
        if (context != NULL) \
        { \
            goto *context; \
        } \
    } while (0) 

#define END \
    do \
    { \
        return 0; \
    } while (0) 

#define YIELD(a) \
    do \
    { \
        context = &&NEW_LABEL; \
        return a; \
    } while (0); \
    NEW_LABEL:

int loop_handler2(int last)
{

    int c = 0;
    int d = 0;

    BEGIN;

    a = 99;
    b = 1000;
    printf("Here we go, setting a = %d and b = %d\n", a, b);

    YIELD(0)

    c = a + b;
    printf("a + b == %d\n", c);

    YIELD(c)

    d = a * b;
    printf("a * b == %d\n", d);

    YIELD(d)

    printf("We are done, bye bye\n");
    END;
}

这里YIELD的调用看起来像是协程的非call/return出入口了,但本质上还是return,因而是stackless的,不能重复使用栈上的变量. 另外熟悉Python的同学可能也会觉得这在形式上和迭代器(Generator)也比较像,而迭代器正是python用于实现协程的主要机制,不是吗?

二、有什么用
后台开发的同学肯定知道,一般的异步服务框架的实现就是不停的回调这样一个loop_handler,然后业务要在回调中实现一个基于状态机的交互过程,当交互过程比较复杂的时候可能需要派生很多的State和Api,代码分散,逻辑跳转复杂不集中不好维护。而利用上述编码方式有时候我们可以方便地将State压缩为1个,在异步框架中用同步的方式编码,集中维护业务逻辑。例如:

//业务要实现的回调,异步server框架要求返回跳转的下一个状态ID,本例中只有一个状态即DefaultState
int SingleState::Process(RequestCtx *ctx)  //当前会话的状态都在这里集中处理
{
    BEGIN;

    int ret = Request_Svr_A(...)   //给服务A发请求
    if (ret != 0)
    {
        LOG_ERROR(...);
        END;  //API调用都失败了就结束吧,下同
    }

    YIELD(DefaultState)  //异步等A回包, 跳转到自身唯一状态
    ...

    if (Check_A_Fail)   //A返回了错误,那么再去请求B
    {
        ret = Request_Svr_B(...)
        if (ret != 0) { END; }

        YIELD(DefaultState)   //异步等B回包
        
        if (Check_B_Fail)  //B也返回了错误
        {
             LOG_ERROR(...);
             END;   //打日志并结束会话
        }
    }
    ...
 
    ret = Request_Svr_C_D(...)  //再并发请求C和D
    YIELD(DefaultState)  //异步等C、D回包

    ret = Request_Svr_E(...) //再与E交互
    YIELD(DefaultState)  //异步等E回包

    ... //处理结果、回包等等
  
    END;  //结束会话
}

在笔者使用的一个网络框架中,如果不按照这样处理,为了满足框架,一般需要为每组交互的服务(A、B、C/D、E)都派生一个State类并实现Process函数,在其中返回下一个状态类ID,因此需要实现的子类较多且逻辑分散;而上面借鉴Protothreads的例子只实现了SingleState一个类,状态转移都蕴含在其中了,业务逻辑集中且清晰,用同步的方式编写异步的代码也比较高效。

当然缺点是需要更加小心因为毕竟不是真正的协程,如不注意Stackless会带来不少坑,但在异步服务器开发这里通常不是问题,只要我们把上述context指针放在请求上下文中,规定中间结果也都统一放在请求上下文中,通常也不需要重复访问局部变量,不同的请求处理之间也互不影响。

三、More
当然实际实现情况不会这么简单, 上述简化版可能只对某类服务框架够用,并且并非是直接存取label,而是利用switch case来达到更灵活的效果,但原理一致。有空可以深入看看Protothreads的通用实现.

发表评论

电子邮件地址不会被公开。