带有共享栈缓存的协程库

1.什么是协程

在多线程和多进程的程序中,线程和进程的调度都是由操作系统完成的,用户没有办法确定其调度和执行的先后顺序,且在每次调度时需要转换到内核态,调度完成后再转回用户态,这是非常费时间的,更不用说多进程程序的调度还可能会造成缓存和TLB失效。
协程(Coroutine)又叫作微线程,是一种比线程更加细粒度的调度单位。它的调度是完全由用户来决定的,换言之,不会在调度中陷入内核态,且什么时候进行调度完全由用户来指定。
可以了解一下python中的的协程
简单的说就是在单协程的程序中,main函数进行其他函数调用后会对调用的函数进行压栈,各类寄存器指向该函数的栈帧,然后执行完成后,将局部变量出栈,通过保留的地址返回main内部继续进行执行,寄存器重新指向main的栈帧,继续main函数的执行。而协程就是在函数调用的过程中,主动的停止这一次函数执行,保留该函数执行的上下文(以后才能通过上下文重新执行),而切换到其他函数的上下文进行执行。
其实整个切换过程和进程与线程的切换是很相似的:都是保留现有上下文,再恢复想要执行的上下文,再执行。不过不同之处在于进程和线程是自动调度的,且有两次内核态与用户态的切换,保存的上下文也远远多于协程的上下文,所以进程线程的切换开销远远大于协程。但是协程的缺点就是不能发挥出多核CPU的威力,同一个线程中的协程是不能并行的(当然也就不会出现数据竞态问题),所以协程编程模型一般使用多进程+多协程。
而在C/C++中并没有直接提供协程,但是有不少库中提供了协程的封装:

  • 第一种是使用ucontext组件实现的,如云风大佬的Coroutine
  • 第二种是利用汇编完成上下文切换,如开源库libco
  • 第三种是利用setjmp和longjmp实现的

现在简单的看一下ucontext结构和其组件的使用方法:

typedef struct ucontext {
	struct ucontext *uc_link;
    sigset_t         uc_sigmask;
    stack_t          uc_stack;
    mcontext_t       uc_mcontext;
    ...
    } ucontext_t;

uc_link指向该上下文执行完成后的后继上下文,uc_sigmask是设置信号掩码,uc_stack是为该上下文设置栈空间,uc_mcontext没使用过,还不太清楚。

int getcontext(ucontext_t *ucp);

初始化ucp结构体,并获取当前上下文。

int setcontext(const ucontext_t *ucp);

将当前上下文切换到ucp

void makecontext(ucontext_t *ucp, void (*func)(), int argc, ...);

为ucp上下文绑定一个执行函数,argc是该函数的参数个数,后面是func的参数序列。

int swapcontext(ucontext_t *oucp, ucontext_t *ucp);

将当前上下文保存到oucp中,然后切换到ucp上下文

来看一个简单的示例:

#include <iostream>
#include <ucontext.h>
using namespace std;
static char g_stack[2048];
static ucontext_t ctx, ctx_main;
void func()
{
	cout << "enter func" << endl;
	swapcontext(&ctx, &ctx_main);
	cout << "func1 resume from yield" << endl;
}

int main()
{
	getcontext(&ctx);
	ctx.uc_stack.ss_sp = g_stack;
	ctx.uc_stack.ss_size = sizeof g_stack;
	ctx.uc_link = &ctx_main;
	makecontext(&ctx, func, 0);
	cout << "in main, before coroutine starts" << endl;
	swapcontext(&ctx_main, &ctx);
	cout << "back to main" << endl;
	swapcontext(&ctx_main, &ctx);
	cout << "back to main again" << endl;
	return 0;
}

执行结果为:
在这里插入图片描述
使用的方式是先通过getcontext初始化上下文结构体,然后为其设置好对应的栈空间及大小,后继上下文,然后使用swapcontext进行上下文切换。

2.什么是共享栈

从上面context的使用中可以看到,每个协程函数的执行都需要一个栈空间,在平时的单协程程序中,栈空间都是由操作系统来分配的,操作系统可以为每个函数分配适量的栈空间,但是在多协程程序中,需要用户来为协程分配栈空间,且栈空间是不可以修改的,分配完之后就不能在扩容了,所以我们需要为每个协程分配足够的栈空间,防止在执行过程中出现栈溢出。比如libco中为每个协程分配128kb的栈空间,但是当并发量很高,有数百万个协程同时运行时,需要使用几百个G的内存。但是其实每个协程所使用的空间其实可能远远小于128k,为了节省空间,就有了共享栈的思想。
共享栈即是为所有协程分配一个足够大的栈空间,所有协程执行是,将上下文拷贝到栈空间中进行执行,当需要切换别的协程时,将当前上下文保存(保存时可以使用堆上空间,需要多少就申请多少),再将别的协程上下文拷贝到共享栈空间中进行执行。这样就可以很大程度的节省内存空间,当然,付出的代价是每次切换时拷贝所需要的时间。

3.共享栈缓存

虽然共享栈减少了内存的使用,但是加大了时间的损耗。为了减少时间上的损耗,我们可以在内存空间允许的范围内,申请多个共享栈空间,每次协程切换时,不需要将旧协程的数据拷贝出来,将新协程的数据拷贝到一个空的共享栈空间时即可。如果将要执行的协程数据仍然在共享栈中,就可以不用拷贝,执行选择对应栈进行执行。当共享栈已经满了,而新的协程需要拷贝进共享栈时,将即将被占用的栈的数据拷贝到到一个堆空间进行保存,堆空间可以按需分配,不会造成浪费。
本来最早的想法是使用LRU置换方法进行调度,但是发现好像不行,因为makecontext之后的上下文context已经绑定在最初设置的栈空间上了,由于不清楚context的底层实现,没法修改相关的寄存器指针。看来有时间还是需要看一看协程切换的汇编实现。
最终我选择的是用哈希的办法,准备一定数量的共享栈,对协程id进行哈希决定其应该放到哪个栈中,这样就可以保证重新唤醒的协程可以在上一次的栈中继续进行执行。

4.简易协程库的实现

#define STACKSIZE (1024*1024)
typedef std::function<void()> Function;
enum  COSTATE { FREE = 0, UNINIT, STARTED };
struct Coroutine
{
	ucontext_t ctx;//协程上下文
	Function func;//协程执行的函数
	COSTATE state;//当前协程状态
	char* stack=NULL;//协程被换出时保存上下文
	int size;//保存的上下文大小
};
class CoManager
{
public:
	CoManager(int coroutinemaxnum = 1024,int cachenum=16);
	~CoManager();
	void coresume(int id);//唤醒对应id的协程
	void coyield();//停止当前协程并返回到主协程
	int cocreate(Function func);//创建一个协程
	bool cofinished();//确认是否所有协程都已经执行完毕
	void codelete(int id);//删除对应id协程并释放对应空间
private:
	vector<Coroutine> coroutines;
	ucontext_t main_ctx;//主协程上下文
	int runid;//正在执行的协程id
	int maxIndex;//使用过的最大协程id
	int cachenum;//缓存共享栈的个数
	stack<int> co_record;
	static void execFunc(CoManager* c);
	vector<pair<char*, int>> shareStackpool;
	char* getsharedstack(int id);//内部使用,帮助协程获取共享栈地址
	bool isInCache(int id);//内部使用,判断协程是否在共享栈中
	void setCostacksize(int id);//计算协程执行栈的长度
};

下面简单介绍几个比较重要的函数:

char * CoManager::getsharedstack(int id)
{
	if (shareStackpool[id%shareStackpool.size()].second == id)//如果在缓存中,直接使用
	{
		return shareStackpool[id%shareStackpool.size()].first;
	}
	else
	{
		if (shareStackpool[id%shareStackpool.size()].second == -1)//如果还没有使用过
		{
			shareStackpool[id%shareStackpool.size()].second = id;
			return shareStackpool[id%shareStackpool.size()].first;
		}
		else//如果被别人使用了
		{
			int currid = shareStackpool[id%shareStackpool.size()].second;
			if (coroutines[currid].stack != NULL)
				free(coroutines[currid].stack);
			coroutines[currid].stack = (char*)malloc(coroutines[currid].size);
			memcpy(coroutines[currid].stack, shareStackpool[id%shareStackpool.size()].first + STACKSIZE - coroutines[currid].size, coroutines[currid].size);
			shareStackpool[id%shareStackpool.size()].second = id;
			return shareStackpool[id%shareStackpool.size()].first;
		}
	}
 }

该函数是用来为将要执行的协程获取共享栈空间的,如果该协程上下文在共享缓存区中,那么直接继续执行即可,如果不在缓存区中,则将其放入缓冲区并置换出对应的数据开辟一个堆空间进行保存。

//停止当前协程,返回上一级协程
void CoManager::coyield()
{
	if (!co_record.empty())//在主协程中yield无效
	{
		int id = co_record.top();
		co_record.pop();
		const ucontext_t* nextctx = NULL;
		if (co_record.empty())
		{
			
			nextctx = &main_ctx;
			runid = -1;
		}
		else
		{
			runid = co_record.top();
			nextctx = &coroutines[co_record.top()].ctx;
			bool isincache = isInCache(id);
			char* stack = getsharedstack(id);
			coroutines[runid].ctx.uc_stack.ss_sp = stack;
			if (!isincache)
				memcpy(stack + STACKSIZE - coroutines[runid].size, coroutines[runid].stack, coroutines[runid].size);
		}
		//savestack(id);
		setCostacksize(id);//只设置好栈的长度,暂时不将数据移出共享区
		swapcontext(&coroutines[id].ctx, nextctx);
	}
}

在协程停止时,不会立即将共享区的数据拷贝到对应协程的上下文保存区中,也不会立即删除共享区内部的协程上下文,因为可能下次还会使用,直到有别的协程使用这个区间时才会将其移出共享区。

void CoManager::coresume(int id)
{
	if (id<0 || id>maxIndex)
		return;
	ucontext_t* currctx = NULL;
	if (co_record.empty())
	{
		currctx = &main_ctx;
	}
	else
	{
		currctx = &coroutines[co_record.top()].ctx;
	}
	co_record.push(id);
	switch (coroutines[id].state)
	{
	case UNINIT:
	{
		getcontext(&coroutines[id].ctx);
		coroutines[id].ctx.uc_stack.ss_sp = getsharedstack(id);//sharedstack;
		coroutines[id].ctx.uc_stack.ss_size = STACKSIZE;
		coroutines[id].ctx.uc_link = currctx;
		runid = id;
		coroutines[id].state = STARTED;
		makecontext(&coroutines[id].ctx, (void(*)(void))execFunc, 1, this);
		swapcontext(currctx, &coroutines[id].ctx);
		break;
	}
	case STARTED:
	{
		bool isincache = isInCache(id);
		char* stack = getsharedstack(id);
		coroutines[id].ctx.uc_stack.ss_sp = stack;
		
		if (!isincache)//如果不在缓存中,复制将缓存区文件复制到工作区
		{
			memcpy(stack + STACKSIZE - coroutines[id].size, coroutines[id].stack, coroutines[id].size);
		}
		runid = id;

		coroutines[id].ctx.uc_link = currctx;
		
		swapcontext(currctx, &coroutines[id].ctx);
		break;
	}
	default:
		break;
	}
}

如果是还未运行过的协程需要为其制定后继上下文和共享栈的的长度,如果是已经执行过的,那么直接获取对应共享区即可,如果是任然在缓存区中,那么不需要拷贝数据进缓存区。

注释已经写得很多了,就不多逼逼了,完整代码放在github

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值