struct args {
int n;
};
static void foo(struct schedule * S, void *ud)
{
// 将传入的void指针转换为args结构体指针
struct args * arg = ud;
// 获取args结构体中的n成员变量的值,赋值给start变量
int start = arg->n;
int i;
// 循环5次
for (i=0;i<5;i++) {
// 打印当前运行的协程编号和start+i的值
printf("coroutine %d : %d\n",coroutine_running(S) , start + i);
coroutine_yield(S);
}
}
static void test(struct schedule *S) {
struct args arg1 = { 0 };
struct args arg2 = { 100 };
// 创建两个协程,分别传入不同的参数
int co1 = coroutine_new(S, foo, &arg1);
int co2 = coroutine_new(S, foo, &arg2);
// 打印主函数开始信息
printf("main start\n");
// 当两个协程都未结束时,循环执行以下操作
while (coroutine_status(S,co1) && coroutine_status(S,co2)) {
coroutine_resume(S,co1);
coroutine_resume(S,co2);
}
printf("main end\n");
}
int main()
{
// 打开协程调度器
struct schedule * S = coroutine_open();
// 调用测试函数
test(S);
// 关闭协程调度器
coroutine_close(S);
return 0;
}
这个示例代码很简短,首先调用coroutine_open()创建了协程调度器,然后运行test。在test中,coroutine_new创建了两个协程co1和co2,不断的yield和resume到协程执行完毕。
在test的while循环中加入输出,可以看到coroutine_resume之后协程才开始执行,yield后就退出了,下次切换回来从yield后执行。
这里有几个核心部分:
1、协程调度器创建coroutine_open
2、协程任务创建coroutine_new
3、协程任务唤醒coroutine_resume
4、协程任务让出coroutine_yield
struct schedule {
char stack[STACK_SIZE];
ucontext_t main;
int nco;
int cap;
int running;
struct coroutine **co;
};
struct coroutine **co是一个数组,存放着所有协程
ucontext_t main 主协程的上下文,方便后续切换回主协程
char stack[STACK_SIZE] 所有协程运行时的共享栈
struct schedule * coroutine_open(void) {
struct schedule *S = malloc(sizeof(*S));
S->nco = 0;
S->cap = DEFAULT_COROUTINE;
S->running = -1;
S->co = malloc(sizeof(struct coroutine *) * S->cap);
memset(S->co, 0, sizeof(struct coroutine *) * S->cap);
return S;
}
void coroutine_close(struct schedule *S) {
int i;
for (i=0;i<S->cap;i++) {
struct coroutine * co = S->co[i];
if (co) {
_co_delete(co);
}
}
free(S->co);
S->co = NULL;
free(S);
}
int coroutine_new(struct schedule *S, coroutine_func func, void *ud) {
// 创建一个新的协程对象
struct coroutine *co = _co_new(S, func , ud);
// 如果协程数量大于等于容量
if (S->nco >= S->cap) {
// 记录当前容量
int id = S->cap;
// 将协程数组容量翻倍
S->co = realloc(S->co, S->cap * 2 * sizeof(struct coroutine *));
// 将新扩容的部分初始化为0
memset(S->co + S->cap , 0 , sizeof(struct coroutine *) * S->cap);
// 将新创建的协程对象添加到数组中
S->co[S->cap] = co;
// 更新容量
S->cap *= 2;
// 协程数量加1
++S->nco;
// 返回新创建的协程的id
return id;
} else {
int i;
for (i=0;i<S->cap;i++) {
//从第nco个开始,策略和bthread一样,但是循环用cap取余,brpc中到最后再从0开始
int id = (i+S->nco) % S->cap;
// 如果当前位置为空
if (S->co[id] == NULL) {
// 将新创建的协程对象添加到数组中
S->co[id] = co;
// 协程数量加1
++S->nco;
// 返回新创建的协程的id
return id;
}
}
}
assert(0);
return -1;// 返回-1表示创建协程失败
}
首先是创建协程_co_new
struct coroutine * _co_new(struct schedule *S , coroutine_func func, void *ud) {
// 分配内存空间给协程结构体
struct coroutine * co = malloc(sizeof(*co));
// 设置协程函数
co->func = func;
// 设置协程用户数据
co->ud = ud;
co->sch = S;
co->cap = 0;
co->size = 0;
// 设置协程状态为就绪
co->status = COROUTINE_READY;
co->stack = NULL;
return co;
}
struct coroutine {
coroutine_func func;//协程任务函数
void *ud;//参数
ucontext_t ctx;//上下文
struct schedule * sch;//调度器
ptrdiff_t cap;//已经分配内存大小
ptrdiff_t size;//运行时栈大小
int status;//协程状态
char *stack;//保存起来的运行栈
};
_co_new函数首先是创建一个协程的结构体(分配空间并复制常规的create&init)。然后判断当前调度器管理的协程数量是否超出容量cap,超出就扩容,没超出就在S->co数组中找个空位存放协程的指针。
这样协程创建好后就是ready,并没有开始执行。
按照demo顺序往前看,coroutine_new之后检测coroutine_status(存储在coroutine结构体的status:0123),然后执行coroutine_resume
void coroutine_resume(struct schedule * S, int id) {
// 断言,检查调度器状态
assert(S->running == -1);
// 断言,确保传入的协程ID是有效的
assert(id >=0 && id < S->cap);
// 获取指定ID的协程结构体指针
struct coroutine *C = S->co[id];
// 如果协程为空,则直接返回
if (C == NULL)
return;
// 获取协程的状态
int status = C->status;
// 根据协程状态进行不同的处理
switch(status) {
case COROUTINE_READY:
// 获取协程的上下文
getcontext(&C->ctx);
// 设置协程的栈空间
C->ctx.uc_stack.ss_sp = S->stack;
C->ctx.uc_stack.ss_size = STACK_SIZE;
// 设置协程的上下文链接
C->ctx.uc_link = &S->main;
// 设置当前正在运行的协程ID
S->running = id;
// 更新协程的状态为运行中
C->status = COROUTINE_RUNNING;
// 将S的地址转换为无符号整型
uintptr_t ptr = (uintptr_t)S;
// 设置协程的上下文,指定要执行的函数为mainfunc,并传入两个参数
makecontext(&C->ctx, (void (*)(void)) mainfunc, 2, (uint32_t)ptr, (uint32_t)(ptr>>32));
// 交换上下文,切换到协程的上下文执行
swapcontext(&S->main, &C->ctx);
break;
case COROUTINE_SUSPEND:
// 将协程的栈内容复制到调度器的栈空间中
memcpy(S->stack + STACK_SIZE - C->size, C->stack, C->size);
// 设置当前正在运行的协程ID
S->running = id;
// 更新协程的状态为运行中
C->status = COROUTINE_RUNNING;
// 交换上下文,切换到协程的上下文执行
swapcontext(&S->main, &C->ctx);
break;
default:
// 断言失败,表示协程状态不合法
assert(0);
}
}
前面各种判断扫一眼就过,重点关注对不同状态的switch操作。
从ready->running的协程:
getcontext(&C->ctx) 初始化 ucontext_t 结构体,将当前的上下文放到 C->ctx 里面
C->ctx.uc_stack.ss_sp = S->stack 设置当前协程的运行时栈,这里用的是调度器的共享栈
C->ctx.uc_link = &S->main 如果协程执行完,则切换到S->main主协程中进行执行。如果不设置, 则默认为 NULL,那么协程执行完,整个程序就结束了。
makecontext(&C->ctx, (void (*)(void)) mainfunc, 2, (uint32_t)ptr, (uint32_t)(ptr>>32)) 设置ucontext执行函数,后面两个参数是把调度器指针拆分两个uint_32。
static void mainfunc(uint32_t low32, uint32_t hi32) {
// 将低32位和高32位合并为一个64位指针
uintptr_t ptr = (uintptr_t)low32 | ((uintptr_t)hi32 << 32);
struct schedule *S = (struct schedule *)ptr;
int id = S->running;
struct coroutine *C = S->co[id];
C->func(S,C->ud);
_co_delete(C);
S->co[id] = NULL;
--S->nco;
S->running = -1;
}
swapcontext(&S->main, &C->ctx) 交换两个指针的内容(上下文)。将当前上下文存放到S->main,并将C->ctx的上下文替换到当前。在coroutine中开始执行mainfunc。
void coroutine_yield(struct schedule * S) {
int id = S->running;
assert(id >= 0);
struct coroutine * C = S->co[id];
// 断言确保协程的结构体地址在栈之上
assert((char *)&C > S->stack);
// 保存当前协程的栈信息
_save_stack(C,S->stack + STACK_SIZE);
// 将协程状态设置为挂起
C->status = COROUTINE_SUSPEND;
// 将运行中的协程ID设置为-1,表示当前没有协程在运行
S->running = -1;
// 切换上下文,将当前协程的上下文保存到C->ctx中,并加载主协程的上下文S->main
swapcontext(&C->ctx , &S->main);
}
调用 coroutine_yield 可以使当前正在运行的协程切换到主协程中运行。此时,该协程会进入 SUSPEND 状态
coroutine_yield 的具体实现依赖于两个行为:
- 调用 _save_stack 将当前协程的栈保存起来。因为 coroutine 是基于共享栈的,所以协程的栈内容需要单独保存起来。
- swapcontext 将当前上下文保存到当前协程的 ucontext 里面,同时替换当前上下文为主协程的上下文。 这样的话,当前协程会被挂起,主协程会被继续执行。
栈的生长方向是从高地址往低地址。只要找到栈的栈顶和栈底的地址,就可以找到整个栈内存空间了。在coroutine中因为协程的运行时栈的内存空间是自己分配的。在coroutine_resume阶段设置了 C->ctx.uc_stack.ss_sp = S->stack。根据以上理论,栈的生长方向是高地址到低地址,因此栈底的就是内存地址最大的位置,即 S->stack + STACK_SIZE 就是栈底位置。
static void _save_stack(struct coroutine *C, char *top) {
char dummy = 0;
// 确保栈顶地址与dummy地址之差不超过STACK_SIZE
assert(top - &dummy <= STACK_SIZE);
// 如果当前栈容量小于地址差(cap会在第一次save的时候赋值,就是运行时栈大小)
//
if (C->cap < top - &dummy) {
// 释放当前栈内存
free(C->stack);
// 更新栈容量为栈顶地址与dummy地址之差
C->cap = top-&dummy;
// 分配新的栈内存
C->stack = malloc(C->cap);
}
// 更新栈大小为栈顶地址与dummy地址之差
C->size = top - &dummy;
// 将栈内容从dummy地址复制到栈内存中
memcpy(C->stack, &dummy, C->size);
}
dummy是刚定义的变量,理应在栈最顶部。
栈的大小就是S->stack + STACK_SIZE - &dummy。
SUSPEND->running,因为之前设置过上下文,所以这部分只有两个关键操作:
memcpy(S->stack + STACK_SIZE - C->size, C->stack, C->size) 在 yield 的时候,协程的栈内容保存到了 C->stack 数组中。用 memcpy 把协程的之前保存的栈内容,重新拷贝到运行时栈里面。拷贝的开始位置,需要简单计算下S->stack + STACK_SIZE - C->size这个位置就是之前协程的栈顶位置。
swapcontext(&S->main, &C->ctx) 交换上下文。
共享栈
共享栈在libco中提到很多,其实这里coroutine也是共享栈模型,共享栈本质就是所有协程在运行的时候都是用同一个栈空间,
有共享栈就有非共享栈,每个协程的栈都是独立的,固定大小。切换的时候不用来回拷贝,但是也会浪费内存空间。
因为栈空间在运行时不能随时扩容,所以需要预先开一个足够的栈空间,很多协程运行时用不了这么大空间,就必然造成内存的浪费
coroutine默认分配的栈空间是1M,所有栈运行都是用这个空间。
C->ctx.uc_stack.ss_sp = S->stack;
C->ctx.uc_stack.ss_size = STACK_SIZE;
这两行对context设置的就是运行时栈,
调用yield时,协程的栈内容就被保存起来,保存的时候用多少就开多少(_save_task)。
resume时就将之前保存的栈内容重新拷贝到运行时栈中。
总结
核心流程:
co_resume()中给co->ctx赋值并设置(makecontext)task_run函数,然后swapcontext(&S->_main_ctx,&co->ctx)保存当前上下文到S->_main_ctx,跳转到task_run执行。
task_run中执行co->func,func中coro_yield。
coro_yield执行save_stack,计算当前协程的运行栈大小并分配空间、保存运行栈数据。回到coro_yield后swapcontext(&co->ctx,&S->_main_ctx);保存当前上下文,并跳转到S->_main_ctx的流程中,其实就是回到co_resume。
其它协程也是一样创建执行。
test第二次循环执行到co_resume后,是从暂停恢复。memcpy(S->s_stack + STACK_SIZE - co->stack_size,co->stack,co->stack_size);将保存在co->stack的运行栈数据拷贝到共享栈中(S->s_stack + STACK_SIZE - co->stack_size是第一次执行的位置)。在执行swapcontext保存当前上下文到main_ctx,跳转到co->ctx的task_run。
然后继续执行co->func,yield又到save_stack,co->stack_cap在第一次执行时已经赋值并分配内存空间。协程创建后容量大小一般不变,但是执行过程中运行时大小可能因为变化,如果stack_size减小,memcpy的时候复制大小更精准,避免不必要的复制。然后swapcontext重复