用C实现协程库
wptr33 2025-04-27 16:40 13 浏览
协程这个东西有一段时间非常火热,特别是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 <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stddef.h>
#include <string.h>
#include <stdint.h>
#if __APPLE__ && __MACH__
#include <sys/ucontext.h>
#else
#include <ucontext.h>
#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;i<S->cap;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;i<S->cap;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语言|实现协程框架,底层原理与性能分析,面试利刃-学习视频教程-腾讯课堂
- 上一篇:盘点c++几种常见的设计模式及具体实现
- 下一篇:C++基础知识总结(超详细总结)
相关推荐
- 每天一个编程技巧!掌握这7个神技,代码效率飙升200%
-
“同事6点下班,你却为改BUG加班到凌晨?不是你不努力,而是没掌握‘偷懒’的艺术!本文揭秘谷歌工程师私藏的7个编程神技,每天1分钟,让你的代码从‘能用’变‘逆天’。文末附《Python高效代码模板》,...
- Git重置到某个历史节点(Sourcetree工具)
-
前言Sourcetree回滚提交和重置当前分支到此次提交的区别?回滚提交是指将改动的代码提交到本地仓库,但未推送到远端仓库的时候。...
- git工作区、暂存区、本地仓库、远程仓库的区别和联系
-
很多程序员天天写代码,提交代码,拉取代码,对git操作非常熟练,但是对git的原理并不甚了解,借助豆包AI,写个文章总结一下。Git的四个核心区域(工作区、暂存区、本地仓库、远程仓库)是版本控制的核...
- 解锁人生新剧本的密钥:学会让往事退场
-
开篇:敦煌莫高窟的千年启示在莫高窟321窟的《降魔变》壁画前,讲解员指着斑驳色彩说:"画师刻意保留了历代修补痕迹,因为真正的传承不是定格,而是流动。"就像我们的人生剧本,精彩章节永远...
- Reset local repository branch to be just like remote repository HEAD
-
技术背景在使用Git进行版本控制时,有时会遇到本地分支与远程分支不一致的情况。可能是因为误操作、多人协作时远程分支被更新等原因。这时就需要将本地分支重置为与远程分支的...
- Git恢复至之前版本(git恢复到pull之前的版本)
-
让程序回到提交前的样子:两种解决方法:回退(reset)、反做(revert)方法一:gitreset...
- 如何将文件重置或回退到特定版本(怎么让文件回到初始状态)
-
技术背景在使用Git进行版本控制时,经常会遇到需要将文件回退到特定版本的情况。可能是因为当前版本出现了错误,或者想要恢复到之前某个稳定的版本。Git提供了多种方式来实现这一需求。...
- git如何正确回滚代码(git命令回滚代码)
-
方法一,删除远程分支再提交①首先两步保证当前工作区是干净的,并且和远程分支代码一致$gitcocurrentBranch$gitpullorigincurrentBranch$gi...
- [git]撤销的相关命令:reset、revert、checkout
-
基本概念如果不清晰上面的四个概念,请查看廖老师的git教程这里我多说几句:最开始我使用git的时候,我并不明白我为什么写完代码要用git的一些列指令把我的修改存起来。后来用多了,也就明白了为什么。gi...
- 利用shell脚本将Mysql错误日志保存到数据库中
-
说明:利用shell脚本将MYSQL的错误日志提取并保存到数据库中步骤:1)创建数据库,创建表CreatedatabaseMysqlCenter;UseMysqlCenter;CREATET...
- MySQL 9.3 引入增强的JavaScript支持
-
MySQL,这一广泛采用的开源关系型数据库管理系统(RDBMS),发布了其9.x系列的第三个更新版本——9.3版,带来了多项新功能。...
- python 连接 mysql 数据库(python连接MySQL数据库案例)
-
用PyMySQL包来连接Python和MySQL。在使用前需要先通过pip来安装PyMySQL包:在windows系统中打开cmd,输入pipinstallPyMySQL ...
- mysql导入导出命令(mysql 导入命令)
-
mysql导入导出命令mysqldump命令的输入是在bin目录下.1.导出整个数据库 mysqldump-u用户名-p数据库名>导出的文件名 mysqldump-uw...
- MySQL-SQL介绍(mysql sqlyog)
-
介绍结构化查询语言是高级的非过程化编程语言,允许用户在高层数据结构上工作。它不要求用户指定对数据的存放方法,也不需要用户了解具体的数据存放方式,所以具有完全不同底层结构的不同数据库系统,可以使用相同...
- MySQL 误删除数据恢复全攻略:基于 Binlog 的实战指南
-
在MySQL的世界里,二进制日志(Binlog)就是我们的"时光机"。它默默记录着数据库的每一个重要变更,就像一位忠实的史官,为我们在数据灾难中提供最后的救命稻草。本文将带您深入掌握如...
- 一周热门
-
-
C# 13 和 .NET 9 全知道 :13 使用 ASP.NET Core 构建网站 (1)
-
因果推断Matching方式实现代码 因果推断模型
-
git pull命令使用实例 git pull--rebase
-
面试官:git pull是哪两个指令的组合?
-
git 执行pull错误如何撤销 git pull fail
-
git pull 和git fetch 命令分别有什么作用?二者有什么区别?
-
git fetch 和git pull 的异同 git中fetch和pull的区别
-
git pull 之后本地代码被覆盖 解决方案
-
还可以这样玩?Git基本原理及各种骚操作,涨知识了
-
git命令之pull git.pull
-
- 最近发表
-
- 每天一个编程技巧!掌握这7个神技,代码效率飙升200%
- Git重置到某个历史节点(Sourcetree工具)
- git工作区、暂存区、本地仓库、远程仓库的区别和联系
- 解锁人生新剧本的密钥:学会让往事退场
- Reset local repository branch to be just like remote repository HEAD
- Git恢复至之前版本(git恢复到pull之前的版本)
- 如何将文件重置或回退到特定版本(怎么让文件回到初始状态)
- git如何正确回滚代码(git命令回滚代码)
- [git]撤销的相关命令:reset、revert、checkout
- 利用shell脚本将Mysql错误日志保存到数据库中
- 标签列表
-
- git pull (33)
- git fetch (35)
- mysql insert (35)
- mysql distinct (37)
- concat_ws (36)
- java continue (36)
- jenkins官网 (37)
- mysql 子查询 (37)
- python元组 (33)
- mybatis 分页 (35)
- vba split (37)
- redis watch (34)
- python list sort (37)
- nvarchar2 (34)
- mysql not null (36)
- hmset (35)
- python telnet (35)
- python readlines() 方法 (36)
- munmap (35)
- docker network create (35)
- redis 集合 (37)
- python sftp (37)
- setpriority (34)
- c语言 switch (34)
- git commit (34)