百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分析 > 正文

用C实现协程库

liebian365 2025-02-15 16:24 2 浏览 0 评论

协程这个东西有一段时间非常火热,特别是Go出来以后,大家都觉得这个用户态线程技术解决了很多问题,甚至用它可以支撑8亿用户,于是大家纷纷写了C/C++的协程库。实际上,我觉得协程库和支撑多少用户关系不大,甚至不用协程还可以支撑更多的用户(减少了协程的开销),协程只是提供一种编程模式,让服务器程序写起来感觉轻松一些。

我们这个协程库,首先它只是一个玩具,我也没有把它用在生产环境中(如果要用我会直接用Go),写这个协程库纯粹是为了学习。

其次,这个库脱胎于云风的协程库,不过云风的协程库更像一个玩具,如果你想知道协程应该怎么实现,看看这个入门是很不错的,代码非常简洁。但这个库也有这些缺点:

  • 功能不大完整,只能支持主协程和协程的切换,无法在协程里面创建协程并启动它。
  • 使用的是共享栈的方式,即所有协程只使用一个栈,协程暂停时,需要把用到的栈内存暂时保存起来,等要运行,再把保存的栈内存拷贝回协程执行的栈。这种方式在resume和yield时,会不断的拷贝内存,效率上会有问题。
  • 环境的切换使用ucontext,因为是系统调用,可能在性能上会有一点点影响,这个没有具体测试过不好下绝对的定论。

我fork过来的修改版在这里,代码改得比较多,这份实现逻辑上更加接近于lua的协程库:

  • 首先是支持协程里启动协程,比如A resume B => B resume C => C yield 返回 B => B yeild 返回 A。
  • 协程的状态也和Lua保持一致:
    • CO_STATUS_SUSPEND 协程创建后未resume,或yield后处的状态
    • CO_STATUS_RUNNING 协程当前正在运行
    • CO_STATUS_NORMAL 当前协程resume了其他协程后处于这个状态
    • CO_STATUS_DEAD 协程执行结束

  • 没有使用共享栈的方式,我的考虑是这样的:
    • 在实际经验中,栈的内存使用其实不多,如果我们默认分配每个栈128K内存,8000个协程才需要1G的虚拟内存,实际的物理内存肯定是更少的。不共享栈,减少了栈内存拷贝的开销,效率会有很明显的提升,也就是典型的空间换时间。
    • 即使要优化,也很容易实现,即对协程分组,每组协程共享一个栈,算是时间和空间上一个权衡,但实际效果究竟如何,有兴趣的人自行测试吧。
  • 协程创建出来后,即使执行完毕,也不释放,只给他标记一个CO_STATUS_DEAD状态,后面创建的协程可以重用,这样减少频繁创建协程的开销。
  • 执行环境的切换,使用的仍然是ucontext,因为我不确定使用ucontext带来的开销到底有多少,但ucontext的好处是支持很多硬件;如果要自己写,通常也只能支持i386和x86_x64两种架构,真的在生产环境中遇到瓶颈再换实现也不迟。

代码量不多,我直接贴在这时,也可以到github上去取:

// coroutine.h
#ifndef C_COROUTINE_H
#define C_COROUTINE_H

// 协程执行结束
#define CO_STATUS_DEAD 0
// 协程创建后未resume,或yield后处的状态
#define CO_STATUS_SUSPEND 1
// 协程当前正在运行
#define CO_STATUS_RUNNING 2
// 当前协程resume了其他协程,此时处于这个状态
#define CO_STATUS_NORMAL 3

// 类型声明
struct schedule;
typedef struct schedule schedule_t;
typedef void (*co_func)(schedule_t *, void *ud);

// 打开一个调度器,每个线程一个:stsize为栈大小,传0为默认
schedule_t * co_open(int stsize);
// 关闭调度器
void co_close(schedule_t *);
// 新建协程
int co_new(schedule_t *, co_func, void *ud);
// 启动协程
int co_resume(schedule_t *, int id);
// 取协程状态
int co_status(schedule_t *, int id);
// 取当前正在运行的协程ID
int co_running(schedule_t *);
// 调用yield让出执行权
int co_yield(schedule_t *);

#endif

实现

// coroutine.c
#include "coroutine.h"
#include 
#include 
#include 
#include 
#include 
#include  
#if __APPLE__ && __MACH__
    #include 
#else 
    #include 
#endif

#define MIN_STACK_SIZE (128*1024)
#define MAX_STACK_SIZE (1024*1024)
#define DEFAULT_COROUTINE 128
#define MAIN_CO_ID 0

#define MIN(a, b) ((a) > (b) ? (b) : (a))
#define MAX(a, b) ((a) < (b) ? (b) : (a))

struct coroutine;

// 每个线程的调度器
typedef struct schedule {
    int stsize;             // 栈大小
    int nco;                // 当前有几个协程
    int cap;                // 协程数组容量
    int running;            // 当前正在运行的协程ID
    struct coroutine **co;  // 协程数组
} schedule_t;

// 协程数据
typedef struct coroutine {
    co_func func;           // 协程回调函数
    void *ud;               // 用户数据
    int pco;                // 前一个协程,即resume这个协程的那个协程
    ucontext_t ctx;         // 协程的执行环境
    schedule_t * sch;       // 调度器
    int status;             // 当前状态:CO_STATUS_RUNNING...
    char *stack;            // 栈内存
} coroutine_t;

schedule_t *co_open(int stsize) {
    schedule_t *S = malloc(sizeof(*S));
    S->nco = 0;
    S->stsize = MIN(MAX(stsize, MIN_STACK_SIZE), MAX_STACK_SIZE);
    S->cap = DEFAULT_COROUTINE;
    S->co = malloc(sizeof(coroutine_t *) * S->cap);
    memset(S->co, 0, sizeof(coroutine_t *) * S->cap);

    // 创建主协程
    int id = co_new(S, NULL, NULL);
    assert(id == MAIN_CO_ID);
    // 主协程为运行状态
    coroutine_t *co = S->co[MAIN_CO_ID];
    co->status = CO_STATUS_RUNNING;
    S->running = id;
    return S;
}

void co_close(schedule_t *S) {
    assert(S->running == MAIN_CO_ID);
    int i;
    for (i=0;icap;i++) {
        coroutine_t * co = S->co[i];
        if (co) {
            free(co->stack);
            free(co);
        }
    }
    free(S->co);
    S->co = NULL;
    free(S);
}

static void cofunc(uint32_t low32, uint32_t hi32) {
    uintptr_t ptr = (uintptr_t)low32 | ((uintptr_t)hi32 << 32);
    schedule_t *S = (schedule_t *)ptr;
    int id = S->running;
    coroutine_t *co = S->co[id];
    co->func(S, co->ud);
    // 标记协程为死亡
    co->status = CO_STATUS_DEAD;
    --S->nco;
    // 恢复前一个协程
    coroutine_t *pco = S->co[co->pco];
    pco->status = CO_STATUS_RUNNING;
    S->running = co->pco;
    ucontext_t dummy;
    swapcontext(&dummy, &pco->ctx);
}

int co_new(schedule_t *S, co_func func, void *ud) {
    int cid = -1;
    if (S->nco >= S->cap) {
        cid = S->cap;
        S->co = realloc(S->co, S->cap * 2 * sizeof(coroutine_t *));
        memset(S->co + S->cap , 0 , sizeof(coroutine_t *) * S->cap);
        S->cap *= 2;
    } else {
        int i;
        for (i=0;icap;i++) {
            int id = (i+S->nco) % S->cap;
            if (S->co[id] == NULL) {
                cid = id;
                break;
            } 
            else if (S->co[id]->status == CO_STATUS_DEAD) {
                // printf("reuse dead coroutine: %d\n", id);
                cid = id;
                break;
            }
        }
    }

    if (cid >= 0) {
        coroutine_t *co;
        if (S->co[cid])
            co = S->co[cid];
        else {
            co = malloc(sizeof(*co));
            co->pco = 0;
            co->stack = cid != MAIN_CO_ID ? malloc(S->stsize) : 0;
            S->co[cid] = co;
        }
        ++S->nco;

        co->func = func;
        co->ud = ud;
        co->sch = S;
        co->status = CO_STATUS_SUSPEND;

        if (func) {
            coroutine_t *curco = S->co[S->running];
            assert(curco);
            getcontext(&co->ctx);
            co->ctx.uc_stack.ss_sp = co->stack;
            co->ctx.uc_stack.ss_size = S->stsize;
            co->ctx.uc_link = &curco->ctx;
            uintptr_t ptr = (uintptr_t)S;
            makecontext(&co->ctx, (void (*)(void))cofunc, 2, (uint32_t)ptr, (uint32_t)(ptr>>32));
        }
    }

    return cid;
}

int co_resume(schedule_t * S, int id) {
    assert(id >=0 && id < S->cap);
    coroutine_t *co = S->co[id];
    coroutine_t *curco = S->co[S->running];
    if (co == NULL || curco == NULL)
        return -1;
    int status = co->status;
    switch(status) {
    case CO_STATUS_SUSPEND:
        curco->status = CO_STATUS_NORMAL;
        co->pco = S->running;
        co->status = CO_STATUS_RUNNING;
        S->running = id;
        swapcontext(&curco->ctx, &co->ctx);
        return 0;
    default:
        return -1;
    }
}

int co_yield(schedule_t * S) {
    int id = S->running;
    // 主协程不能yield
    if (id == MAIN_CO_ID)
        return -1;
    // 恢复当前协程环境
    assert(id >= 0);
    coroutine_t * co = S->co[id];
    coroutine_t *pco = S->co[co->pco];
    co->status = CO_STATUS_SUSPEND;
    pco->status = CO_STATUS_RUNNING;
    S->running = co->pco;
    swapcontext(&co->ctx ,&pco->ctx);
    return 0;
}

int co_status(schedule_t * S, int id) {
    assert(id>=0 && id < S->cap);
    if (S->co[id] == NULL) {
        return CO_STATUS_DEAD;
    }
    return S->co[id]->status;
}

int co_running(schedule_t * S) {
    return S->running;
}

使用方法参考main.c,如果你会用Lua,应该很容易上手,测试代码中有一段是测试创建协程和切换协程的开销的:

int stop = 0;
static void foo_5(schedule_t *S, void *ud) {
    while (!stop) {
        co_yield(S);
    }
}

static void test5(schedule_t *S) {
    printf("test5 start===============\n");
    struct timeval begin;
    struct timeval end;
    int i;
    int count = 10000;

    gettimeofday(&begin, NULL);
    for (i = 0; i < count; ++i) {
        co_new(S, foo_5, NULL);
    }
    gettimeofday(&end, NULL);
    printf("create time=%f\n", timediff(&begin, &end));

    gettimeofday(&begin, NULL);
    for (i =0; i < 1000000; ++i) {
        int co = (i % count) + 1;
        co_resume(S, co);
    }
    gettimeofday(&end, NULL);
    printf("swap time=%f\n", timediff(&begin, &end));

    // 先释放掉原来的
    stop = 1;
    for (i = 0; i < count; ++i) {
        int co = (i % count) + 1;
        co_resume(S, co);
    }
    gettimeofday(&begin, NULL);
    for (i = 0; i < count; ++i) {
        co_new(S, foo_5, NULL);
    }
    gettimeofday(&end, NULL);
    printf("create time2=%f\n", timediff(&begin, &end));
    printf("test5 end===============\n");
}

结果如下,我的虚拟机CPU是双核Intel(R) Xeon(R) CPU E5-2680 v3 @ 2.50GHz

# 第一次创建10000个协程
create time=0.053979s
# 切换200W次协程
swap time=0.883039s
# 第二次创建10000个协程
create time2=0.005390s

这样的性能表现是否能满足要求呢。

再一次声明这个协程库不保证没有BUG,虽然我写了几个测试函数验证过,如果要用在生产环境中,请仔细阅读代码。

推荐一个协程的训练营,之前一直有关注,讲的很不错,内容比较丰富,点这链接有很大的优惠!


纯C语言|实现协程框架,底层原理与性能分析,面试利刃纯C语言|实现协程框架,底层原理与性能分析,面试利刃-学习视频教程-腾讯课堂

相关推荐

4万多吨豪华游轮遇险 竟是因为这个原因……

(观察者网讯)4.7万吨豪华游轮搁浅,竟是因为油量太低?据观察者网此前报道,挪威游轮“维京天空”号上周六(23日)在挪威近海发生引擎故障搁浅。船上载有1300多人,其中28人受伤住院。经过数天的调...

“菜鸟黑客”必用兵器之“渗透测试篇二”

"菜鸟黑客"必用兵器之"渗透测试篇二"上篇文章主要针对伙伴们对"渗透测试"应该如何学习?"渗透测试"的基本流程?本篇文章继续上次的分享,接着介绍一下黑客们常用的渗透测试工具有哪些?以及用实验环境让大家...

科幻春晚丨《震动羽翼说“Hello”》两万年星间飞行,探测器对地球的最终告白

作者|藤井太洋译者|祝力新【编者按】2021年科幻春晚的最后一篇小说,来自大家喜爱的日本科幻作家藤井太洋。小说将视角放在一颗太空探测器上,延续了他一贯的浪漫风格。...

麦子陪你做作业(二):KEGG通路数据库的正确打开姿势

作者:麦子KEGG是通路数据库中最庞大的,涵盖基因组网络信息,主要注释基因的功能和调控关系。当我们选到了合适的候选分子,单变量研究也已做完,接着研究机制的时便可使用到它。你需要了解你的分子目前已有哪些...

知存科技王绍迪:突破存储墙瓶颈,详解存算一体架构优势

智东西(公众号:zhidxcom)编辑|韦世玮智东西6月5日消息,近日,在落幕不久的GTIC2021嵌入式AI创新峰会上,知存科技CEO王绍迪博士以《存算一体AI芯片:AIoT设备的算力新选择》...

每日新闻播报(September 14)_每日新闻播报英文

AnOscarstatuestandscoveredwithplasticduringpreparationsleadinguptothe87thAcademyAward...

香港新巴城巴开放实时到站数据 供科技界研发使用

中新网3月22日电据香港《明报》报道,香港特区政府致力推动智慧城市,鼓励公私营机构开放数据,以便科技界研发使用。香港运输署21日与新巴及城巴(两巴)公司签署谅解备忘录,两巴将于2019年第3季度,开...

5款不容错过的APP: Red Bull Alert,Flipagram,WifiMapper

本周有不少非常出色的app推出,鸵鸟电台做了一个小合集。亮相本周榜单的有WifiMapper's安卓版的app,其中包含了RedBull的一款新型闹钟,还有一款可爱的怪物主题益智游戏。一起来看看我...

Qt动画效果展示_qt显示图片

今天在这篇博文中,主要实践Qt动画,做一个实例来讲解Qt动画使用,其界面如下图所示(由于没有录制为gif动画图片,所以请各位下载查看效果):该程序使用应用程序单窗口,主窗口继承于QMainWindow...

如何从0到1设计实现一门自己的脚本语言

作者:dong...

三年级语文上册 仿写句子 需要的直接下载打印吧

描写秋天的好句好段1.秋天来了,山野变成了美丽的图画。苹果露出红红的脸庞,梨树挂起金黄的灯笼,高粱举起了燃烧的火把。大雁在天空一会儿写“人”字,一会儿写“一”字。2.花园里,菊花争奇斗艳,红的似火,粉...

C++|那些一看就很简洁、优雅、经典的小代码段

目录0等概率随机洗牌:1大小写转换2字符串复制...

二年级上册语文必考句子仿写,家长打印,孩子照着练

二年级上册语文必考句子仿写,家长打印,孩子照着练。具体如下:...

一年级语文上 句子专项练习(可打印)

...

亲自上阵!C++ 大佬深度“剧透”:C++26 将如何在代码生成上对抗 Rust?

...

取消回复欢迎 发表评论: