一则setcontext族函数实验

CGoErlangLinuxlibtask协程性能 by 达达 at 2012-01-02

setcontext族函数是用于实现上下文控制的一组函数,它们可以实现setjmp和longjmp无法实现的高级控制,可以在用户代码级别实现模拟的线程,例如GUN的Pth项目,就是利用这setcontext族函数模拟一个与Pthread兼容的用户代码级别线程库。

最近在研究C/C++中像Erlang那样进行并行计算是否可行,所以研究到了协程。

协程相较于线程,具有消耗小,可控等特点。一个同时在线几千上万人的网络应用,不可能把每个客户端连接都用一个系统线程来对应,但是在Erlang中却可以用Erlang进程来对应每个连接。

将客户端与进程(这里统称Erlang进程和协程)以1:1对应的方式进行设计,这对化简服务器程序设计是非常有用的。

C语言中著名的协程项目libtask正是利用setcontext族函数来实现协程,当目标系统不支持setcontext族函数时才用汇编模拟。

libtask的运作方式与Go语言对并行操作的模拟方式更接近,几乎是一模一样的设计,因为libtask和Go语言都是跟Plan 9有颜渊的,据我了解Go和libtask提供的Channel作为协程间通讯的方式都源自Plan 9。

但是这种方式跟Erlang的Actor模型是不一样的,到底哪种方式更适合C/C++还有待深入研究。

今天做了一个setcontext族函数的实验,用于学习其中几个函数的用法。

实验的内容是分别创建ping和pong两个"协程",一个输出ping,一个输入pong,"调度器"每次上下文切换输出switch,这里协程和调度器打引号是因为直接调用的makecontext和swapcontext函数,看起来一点都不像协程和调度器,但实际上这个实验可以看作是一个最简单的协程调度实验。

完成调度实验后,我加入了上下文切换耗时的计算,分别在我的电脑上(VMWare中的Ubuntu 11.10)和服务器上(Dell R410)进行了实验,10万次上下文切换的耗时在两个实验环境都是0.07秒,我想这个性能足以在一个进程中模拟成千上万的协程了。

最终代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <ucontext.h>

ucontext_t main_ctx, ping_ctx, pong_ctx;

char ping_stack[1024];
char pong_stack[1024];

void ping(void)
{
    for (;;) {
        //printf("ping\n");
        swapcontext(&ping_ctx, &main_ctx);
    }
}

void pong(void)
{
    for (;;) {
        //printf("pong\n");
        swapcontext(&pong_ctx, &main_ctx);
    }
}

int main(int argc, char **argv)
{
    int i, ctx_id = -1;
    clock_t t1, t2, ref;

    getcontext(&ping_ctx);

    ping_ctx.uc_stack.ss_sp = ping_stack;
    ping_ctx.uc_stack.ss_size = sizeof(ping_stack);
    ping_ctx.uc_link = &main_ctx;

    makecontext(&ping_ctx, ping, 0);

    getcontext(&pong_ctx);

    pong_ctx.uc_stack.ss_sp = pong_stack;
    pong_ctx.uc_stack.ss_size = sizeof(pong_stack);
    pong_ctx.uc_link = &main_ctx;

    makecontext(&pong_ctx, pong, 0);

    t1 = clock();

    for (i = 0; i < 100000; i ++) {
    }

    t2 = clock();

    ref = t2 - t1;

    t1 = clock();

    for (i = 0; i < 100000; i ++) {
        //printf("switch, ");

        ctx_id = (ctx_id + 1) % 2;

        if (ctx_id == 0)
            swapcontext(&main_ctx, &ping_ctx);
        else
            swapcontext(&main_ctx, &pong_ctx);
    }

    t2 = clock();

    printf("%lf seconds\n", (double)(t2 - t1 - ref) / CLOCKS_PER_SEC);
}

实验中我遇到两个问题,一个是必须对新的上下文调用setcontext,另一个是新上下文的堆栈必须分配,否则会出现段错误。