一则setcontext族函数实验
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,另一个是新上下文的堆栈必须分配,否则会出现段错误。