协程的实现

用过python或者lua等脚本语言一定对协程印象深刻。这种把异步当同步写的做法很容易被大家接受,而其实协程的起源非常早,最早提出协程概念的 Melvin Conway 是为了解决cobol编译器问题,Conway 构建的这种协同工作机制,需要参与者“让出 (yield)”控制流时,记住自身状态,以便在控制流返回时能够从上次让出的位置恢复(resume)执行。本质是控制流的转换,但是因为命令式编程主流是自顶向下的设计,与协程理念是冲突的也导致协程之前只在一些小众语言实现了。但是随着C10K问题的出现,线程与进程开销无法支撑,linux率先利用epoll非阻塞来解决这个问题,基于事件异步控制流很容易出现callback hell,协程再次进入人们视野。我们先看一个类似协程的例子

def consumer():
    r = ''
    while True:
        n = yield r
        if not n:
            return
        time.sleep(1)
        r = '200 OK'

def produce(c):
    c.next()
    n = 0
    while n < 5:
        n = n + 1
        r = c.send(n)
        dosome(r)
    c.close()

相对于用锁来实现状态的保护,这段代码简直良心。

那协程如何实现 开销多大 以及协程本质是什么

协程切换的本质是寄存器的保存和恢复上次寄存器状态继续执行,协程拥有自己的栈空间,看起来和线程切换也没有什么区别 只是协程是用户在用户态自己实现 线程管理是内核帮忙管理对应用层透明。

我知道的几个协程开源库 libco,云风的coroutine ,还有神人写的用switch-case完成的Protothreads(可以看coolshell 的一个“蝇量级” C 语言协程库) 这几个都是c/c++实现的协程库,这里选择云风的来解释下怎么实现协程以及如何利用c语言的调度原语setjmp来实现

首先来看云风的API设计,云风的API设计参考了lua的用法

#ifndef C_COROUTINE_H
#define C_COROUTINE_H

#define COROUTINE_DEAD 0
#define COROUTINE_READY 1
#define COROUTINE_RUNNING 2
#define COROUTINE_SUSPEND 3

struct schedule;

typedef void (*coroutine_func)(struct schedule *, void *ud);

struct schedule * coroutine_open(void);
void coroutine_close(struct schedule *);

int coroutine_new(struct schedule *, coroutine_func, void *ud);
void coroutine_resume(struct schedule *, int id);
int coroutine_status(struct schedule *, int id);
int coroutine_running(struct schedule *);
void coroutine_yield(struct schedule *);

#endif

很好的c代码设计 不透明指针隐藏了结构体设计 下面来看下调度器和协程结构体设计

struct coroutine;

struct schedule {
	char stack[STACK_SIZE];//共享栈空间
	ucontext_t main;// 保存resume上下文
	int nco;//协程数
	int cap;//协程容量
	int running;// 正在运行的协程id 
	struct coroutine **co;//协程数组
};

struct coroutine {
	coroutine_func func;
	void *ud;//私有参数
	ucontext_t ctx;//运行func上下文
	struct schedule * sch;//调度器
	ptrdiff_t cap;//栈容量
	ptrdiff_t size;//使用栈大小
	int status;//协程状态
	char *stack;//私有栈
};

云风的设计采用的是共享栈,也有一些库是采用私有栈,共享栈优势在于节省空间在调用层次不深情况下 开大量协程内存占用小 缺点是一旦调用层次深可能会大量的分配内存导致性能下降,利用了ucontext组件 具体用法可以参考这个

http://pubs.opengroup.org/onlinepubs/007908799/xsh/ucontext.h.html

这里只分析核心的三个函数 其他函数很好理解 coroutine_resume _save_stack coroutine_yield

void 
coroutine_resume(struct schedule * S, int id) {
	assert(S->running == -1);
	assert(id >=0 && id < S->cap);
	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;
		S->running = id;
		C->status = COROUTINE_RUNNING;
		uintptr_t ptr = (uintptr_t)S;
		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);//拷贝协程的栈到共享栈
		S->running = id;
		C->status = COROUTINE_RUNNING;
		swapcontext(&S->main, &C->ctx);
		break;
	default:
		assert(0);
	}
}

resume 思路是根据需要恢复运行的协程id 取出对应协程,通过协程状态看是初始化ucontext组件 保存运行栈还是拷贝私有协议栈 copy-out来切换栈

swapcontext(main, ctx) 保存resume的上下文 并且切换到ctx上下文执行

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;
	S->running = -1;
	swapcontext(&C->ctx , &S->main);
}

实现了协程悬挂 思路是找到当前运行的协程保存私有栈信息到ctx的私有栈 设置协程状态为悬挂状态 切换到resume执行

这个函数重点是_save_stack

tatic void
_save_stack(struct coroutine *C, char *top) {
	char dummy = 0;
	assert(top - &dummy <= STACK_SIZE);
	if (C->cap < top - &dummy) {
		free(C->stack);
		C->cap = top-&dummy;
		C->stack = malloc(C->cap);
	}
	C->size = top - &dummy;
	memcpy(C->stack, &dummy, C->size);
}

理解这个函数首先要理解栈帧 top是栈顶 从高地址向低地址扩,dummy是哑节点,地址是栈信息的终点,拷贝栈信息到协程私有栈上就完成了copy-in


假如没有ucontext呢 ,我们可以利用c语言的jmpbuf来实现

jmpbuf保存的就是一些寄存器值 不同平台不一样 下面是linux内核的定义如下

/*
 * arch/um/include/sysdep-i386/archsetjmp.h
 */

#ifndef _KLIBC_ARCHSETJMP_H
#define _KLIBC_ARCHSETJMP_H

struct __jmp_buf {
	unsigned int __ebx;
	unsigned int __esp;
	unsigned int __ebp;
	unsigned int __esi;
	unsigned int __edi;
	unsigned int __eip;
};

typedef struct __jmp_buf jmp_buf[1];

#define JB_IP __eip
#define JB_SP __esp

#endif				/* _SETJMP_H */

主要的寄存器 esp,ebp,eip 这里有一个c语言技巧 就是把结构体定义为数组 这样可以当指针用 在堆上直接分配内存

esp 存储栈顶指针 ebp存储当前函数运行状态基地址 eip存储即将运行的指令 知道这几个我们就可以模拟ucontext 比如makecontext 就是设置栈顶指针,保存运行函数地址 实现如下

mark 下次写






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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值