云风coroutine代码跟读

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 的具体实现依赖于两个行为:

  1. 调用 _save_stack 将当前协程的栈保存起来。因为 coroutine 是基于共享栈的,所以协程的栈内容需要单独保存起来。
  2. 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重复

  • 13
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值