C++封装个协程库,基于云风的coroutine

`写了一个简易的协程库,取名为fiber。我的fiber库原型是云风大佬的coroutine库github链接,知乎一位大佬对coroutine库的注释github链接。把大佬们的代码读一遍再抄一遍,体验是真的不一样了。

前言

下面就写下我对协程库的认识。以下操作需要在Linux系统下进行操作,因为用到Linux提供的ucontext.h头文件。在windows下就用不了了.

说实话,这个coroutine库也是个玩具级别的了,优点就是代码行数少,且可以知道协程大概的操作。云风的库相当于只有一个运行栈,确实省空间,但问题是造成代码晦涩难懂。我用C++分别实现了两个版本,一个是完美复刻云风的协程库,一个是按照在知乎上一个老哥的思想,每一个协程一个栈,尽管会造成空间的浪费,但是代码更加易读。

什么是协程?就是在调用某一个函数的时候,可以切出去去执行另一个函数,而不再是一个函数走到尾才能再去执行其他函数。

4大context函数认识一下

getcontext和setcontext

getcontext和 setcontext的使用,getcontext是保存当前的上下文。setcontext是恢复到那个上下文。下面这个demo就是无限递归的例子,因为不断的获取ucontext_t在不断的设置。

#include <stdio.h>
#include <ucontext.h>
#include <unistd.h>
 
int main() {
  ucontext_t context;
 
  //获取当前程序上下文
  getcontext(&context);
  puts("Hello world");
  sleep(1);
  //将程序切换至context指向的上下文处
  setcontext(&context);
  return 0;
}

makecontext和swapcontext

makecontext这个是可以让我们绑定一个回调函数的。比如终于到调用这个上下文了,他会调用这个函数,他还可以带参数,是…的可变参数。extern void makecontext (ucontext_t *__ucp, void (*__func) (void), int __argc, ...) __THROW;函数的原型。切换到那个函数需要一个空间保存呀,这时候我们就要给ucontext_t一个栈空间来保存函数调用所需的空间。

swapcontext函数就是,把当前上下文保存在第一个参数中,然后启动第二个上下文的回调函数。

下面这个demo也是个无限循环,不同的是这个demo是两个函数互相调用

#include <stdio.h>
#include <ucontext.h>
#include <iostream>
#include <unistd.h>
using namespace std;
#define printFun cout << __FUNCTION__ << endl
ucontext_t context1;
ucontext_t context2;
ucontext_t mainContext;
void test1(){
    getcontext(&context1);
    sleep(1);
    printFun;
    setcontext(&context2);
}
void test2(){
    getcontext(&context2);
    sleep(1);
    printFun;
    setcontext(&context1);
    
}

int main(){
    cout << "hello world!" << endl;
    getcontext(&context1);
    char buff1[1024];
    context1.uc_stack.ss_sp = buff1;
    context1.uc_stack.ss_size = sizeof(buff1);
    makecontext(&context1,test1,0);

    getcontext(&context2);
    char buff2[1024];
    context2.uc_stack.ss_sp = buff2;
    context2.uc_stack.ss_size = sizeof(buff2);
    makecontext(&context2,test2,0);

    swapcontext(&mainContext,&context1);

    return 0;
}

结果如下图所示。是不是有点像协程库。
在这里插入图片描述
是不是发现一个问题?我们要怎么让main自动结束阿。这时候就是mainContext的用途啦

修改一下test2函数,将下一个上下文设置为mainContext,在当初的swapcontext保存了mainContext,所以我们可以回到main函数。然后正常结束函数。

#include <stdio.h>
#include <ucontext.h>
#include <iostream>
#include <unistd.h>
using namespace std;
#define printFun cout << __FUNCTION__ << endl
ucontext_t context1;
ucontext_t context2;
ucontext_t mainContext;
void test1(){
    sleep(1);
    printFun;
    setcontext(&context2);
}
void test2(){
    sleep(1);
    printFun;
    setcontext(&mainContext);
}

int main(){
    cout << "hello world!" << endl;
   // setcontext(&context1);
    getcontext(&context1);
    char buff1[1024];
    context1.uc_stack.ss_sp = buff1;
    context1.uc_stack.ss_size = sizeof(buff1);
    makecontext(&context1,test1,0);
    getcontext(&context2);
    char buff2[1024];
    context2.uc_stack.ss_sp = buff2;
    context2.uc_stack.ss_size = sizeof(buff2);
    makecontext(&context2,test2,0);


    swapcontext(&mainContext,&context1);

    return 0;
}

在这里插入图片描述

每根协程一个栈

先把我简化的代码放上来,真的,上来就看共享栈的代码,我猜你可能是蒙B的。相信我。

我们要做什么?
1、创建一个调度器,
2、coroutine_new创建1个协程,coroutine_status判断这根协程的状态。
3、coroutine_resume如果是没运行过的协程,给他分配一块新的内存作为栈的空间(每根协程一个栈空间了)。然后在利用swapcontext执行他的回调函数。
4、在回调函数中,我们可以coroutine_yield将这根协程切出去。

ok,大概模样的就是上述这样。在脑海里有这个思想就很好写啦~代码如下

这两行代码拿出来分析一下,mainfunc尽管可以传无限量参数,但都是int类型阿,还是int32_t的,就是32位的。那我地址要大于2^31还没办法存了?云风大神的做法就是传两个参数,一个是低位,第二个参数右移32位,如果不是0就说明有高位。传过去之后高位在左移32位,如果有高位就不是0了,在一个|(与)操作,easy,完美解决这个问题啦!还有一点是mainfunc是静态函数,不是静态的话无法绑定,因为没有指明这个类的对象。

这个还可以在进行优化,比如销毁一根协程不释放他的栈,只是清空一下,在来一根协程在把这个栈分配给他。这样避免频繁的malloc

void Fiber::mainfunc(uint32_t low32, uint32_t hi32) {
	uintptr_t ptr = (uintptr_t)low32 | ((uintptr_t)hi32 << 32);
	struct schedule* sdu = (struct schedule *)ptr;
}
//coroutine_resume 里的
makecontext(&co->ctx,(void (*)(void))&Fiber::mainfunc,2,(uint32_t)ptr, (uint32_t)(ptr>>32)); 

fiber.h

#pragma once
#include <memory>
#include <functional>
#include "nocopyable.h"

struct schedule;
struct coroutine;


using coroutine_func = std::function<void(void *ud)>;
class Fiber : public nocopyable{
public:
    Fiber();
    ~Fiber() = default;

    // 关闭一个协程调度器
    void coroutine_close();

    // 创建一个协程
    int coroutine_new(coroutine_func, void *ud);

    // 切换到对应协程中执行
    void coroutine_resume(int id);

    // 返回协程状态
    int coroutine_status(int id);

    // 协程是否在正常运行
    int coroutine_running();

    // 切出协程
    void coroutine_yield();

    static void mainfunc(uint32_t low32, uint32_t hi32);

private:
    std::shared_ptr<schedule> sdu;
};

fiber.cpp

#include "fiber.h"

#include <assert.h>
#include <ucontext.h>
#include <vector>
#include <string.h>

using namespace std;

constexpr int STACK_SIZE = 1024 * 128; // 128KB
constexpr int DEFAULT_COROUTINE = 16; // 默认预留16根协程
enum class STATE{
    COROUTINE_DEAD  = 0,
    COROUTINE_READY = 1,
    COROUTINE_RUNNING = 2,
    COROUTINE_SUSPEND  = 3,
};


struct coroutine;
using fiberPtr = shared_ptr<coroutine>;


struct schedule : public nocopyable{
	ucontext_t main; // 主协程的上下文
	int nco = 0; // 当前存活的协程个数
	int cap = DEFAULT_COROUTINE; // 协程管理器的当前最大容量,即可以同时支持多少个协程。如果不够了,则进行扩容
	int running = -1; // 正在运行的协程ID
	vector<fiberPtr> co; // 一个一维数组,用于存放协程
    schedule():co(cap,nullptr){}
	~schedule() = default;
};



/*
* 协程
*/
struct coroutine :public nocopyable{
	coroutine_func func; // 协程所用的函数
	void *ud;  // 协程参数
	ucontext_t ctx; // 协程上下文
	struct schedule * sdu; // 该协程所属的调度器
	int status;	// 协程当前的状态
	char *stack; // 当前协程的保存起来的运行时栈

	coroutine(schedule* sdu,coroutine_func func, void *ud);
	~coroutine();
};

coroutine::coroutine(schedule* sdu,coroutine_func func, void *ud)
					:func(func),ud(ud),sdu(sdu),
					status(static_cast<int>(STATE::COROUTINE_READY)),
					stack(nullptr)
{

}

coroutine::~coroutine(){
	free(stack);
}


Fiber::Fiber():sdu(make_shared<schedule>()){

}

// void Fiber::coroutine_close(struct schedule* sdu){

// }


int Fiber::coroutine_new(coroutine_func fun, void *ud){
	fiberPtr ptr = make_unique<coroutine>(sdu.get(),fun,ud);
	// 下标i当id
	for(int i = 0;i < sdu->co.size();++i){
		if(sdu->co[i] == nullptr){
			sdu->co[i] = std::move(ptr);
			return i;
		}
	}
	// 上面没返回的话走这步
	sdu->co.emplace_back(std::move(ptr));
	return sdu->co.size() - 1;
}

void Fiber::mainfunc(uint32_t low32, uint32_t hi32) {
	uintptr_t ptr = (uintptr_t)low32 | ((uintptr_t)hi32 << 32);
	struct schedule* sdu = (struct schedule *)ptr;
	int id = sdu->running;
	auto co = sdu->co[id];
	// 调用完就关闭
	co->func(co->ud);// 中间可能会被切出去
	co.reset();
	sdu->co[id] = nullptr;
	sdu->running = -1;
}	

// 运行/切换协程
void Fiber::coroutine_resume(int id){
	assert(sdu->running == -1);
	assert(id >=0);

	fiberPtr co = sdu->co[id];
	if(co == nullptr)return ;
	int statu = co->status;
	switch (statu)
	{
		case static_cast<int>(STATE::COROUTINE_READY) :
		{
			getcontext(&co->ctx);
			co->stack = (char*)malloc(STACK_SIZE);
			co->ctx.uc_stack.ss_sp = co->stack;
			co->ctx.uc_stack.ss_size = STACK_SIZE;
			co->ctx.uc_link = &sdu->main;
			
			sdu->running = id;
			co->status = static_cast<int>(STATE::COROUTINE_RUNNING);

			uintptr_t ptr = (uintptr_t)sdu.get();

			makecontext(&co->ctx,(void (*)(void))&Fiber::mainfunc,2,(uint32_t)ptr, (uint32_t)(ptr>>32)); 
			swapcontext(&sdu->main,&co->ctx);
			break;
		}
		case static_cast<int>(STATE::COROUTINE_SUSPEND) : {
			// 相当于把值写在了栈顶,放在调度器上面
			sdu->running = id;
			co->status = static_cast<int>(STATE::COROUTINE_RUNNING);
			swapcontext(&sdu->main,&co->ctx);
			break;
		}
		default:
			assert(0);
			return ;
	}
}


int Fiber::coroutine_status(int id){
	assert(id >= 0 && id < sdu->co.size());
	if(sdu->co[id] == nullptr){
		return static_cast<int>(STATE::COROUTINE_DEAD);
	}
	return sdu->co[id]->status;
}
void Fiber::coroutine_yield(){
	int id = sdu->running;
	assert(id >= 0);

	auto co = sdu->co[id];
	// _save_stack(co.get(),sdu->stack + STACK_SIZE);

	sdu->running = -1;
	co->status = static_cast<int>(STATE::COROUTINE_SUSPEND);

	swapcontext(&co->ctx,&sdu->main);
}
int Fiber::coroutine_running(){
	return sdu->running;
}

test.cpp

#include "fiber.h"
#include <stdio.h>
#include <memory>
#include <iostream>
using namespace std;

struct args {
	int n;
};
int N = 0;
Fiber* fiber;

constexpr int SIZE = 10;

static void foo(void *ud) {
	struct args * arg = (args*)ud;
	int start = arg->n;
	int i;
	for (i=0;i<SIZE;i++) {
		printf("coroutine %d : %d\n",fiber->coroutine_running() , ++N);
		fiber->coroutine_yield();
	}
}

static void test() {
	struct args arg1 = { 0 };
	struct args arg2 = { 100 };

	int co1 = fiber->coroutine_new(foo, &arg1);
	int co2 = fiber->coroutine_new(foo, &arg2);
	printf("main start\n");
	while (fiber->coroutine_status(co1) && fiber->coroutine_status(co2)) {
		fiber->coroutine_resume(co1);
		fiber->coroutine_resume(co2);
	} 
	// printf("arg1 : %d args2 : %d\n",arg1.n,arg2.n);
	cout << "N: " << N << endl;
	printf("main end\n");
}

int main() {
    fiber = new Fiber();
	test();
	
    delete fiber;
	return 0;
}

函数果然是交替执行的,说明我们该动是没有问题
在这里插入图片描述

上面理解了,来看看云风共享栈的操作。每根协程共用调度器的栈。然后记录自己用了多少栈,好进行换入换出操作。多了一个保存栈的操作和恢复栈的操作。

这个是保存栈的操作,让我画一张图来帮助您理解一下这个黑魔法。

static void _save_stack(coroutine* co, char *top) {
	char dumy = 0;
	assert(top - &dumy <= STACK_SIZE);
	if(co->cap < top - &dumy){
		free(co->stack);
		co->cap = top - &dumy;
		co->stack = static_cast<char*>(malloc(co->cap));
	}
	co->size = top - &dumy;
	memcpy(co->stack,&dumy,co->size);
}

在这里插入图片描述

所以top - &dummy 就是这根协程使用的栈啦。只记录他使用的即可

fiber.h

#pragma once
#include <memory>
#include <functional>
#include "nocopyable.h"

struct schedule;
struct coroutine;


using coroutine_func = std::function<void(void *ud)>;
class Fiber : public nocopyable{
public:
    Fiber();
    ~Fiber() = default;

    // 关闭一个协程调度器
    void coroutine_close();

    // 创建一个协程
    int coroutine_new(coroutine_func, void *ud);

    // 切换到对应协程中执行
    void coroutine_resume(int id);

    // 返回协程状态
    int coroutine_status(int id);

    // 协程是否在正常运行
    int coroutine_running();

    // 切出协程
    void coroutine_yield();

    static void mainfunc(uint32_t low32, uint32_t hi32);

private:
    std::shared_ptr<schedule> sdu;
};

fiber.cpp

#include "fiber.h"

#include <assert.h>
#include <ucontext.h>
#include <vector>
#include <string.h>

using namespace std;

constexpr int STACK_SIZE = 1024 * 1024; // 1MB
constexpr int DEFAULT_COROUTINE = 16; // 默认预留16根协程
enum class STATE{
    COROUTINE_DEAD  = 0,
    COROUTINE_READY = 1,
    COROUTINE_RUNNING = 2,
    COROUTINE_SUSPEND  = 3,
};


struct coroutine;
using fiberPtr = shared_ptr<coroutine>;


struct schedule : public nocopyable{
	char stack[STACK_SIZE];	// 运行时栈
	ucontext_t main; // 主协程的上下文
	int nco = 0; // 当前存活的协程个数
	int cap = DEFAULT_COROUTINE; // 协程管理器的当前最大容量,即可以同时支持多少个协程。如果不够了,则进行扩容
	int running = -1; // 正在运行的协程ID
	vector<fiberPtr> co; // 一个一维数组,用于存放协程
    schedule():co(cap,nullptr){}
	~schedule() = default;
};



/*
* 协程
*/
struct coroutine :public nocopyable{
	coroutine_func func; // 协程所用的函数
	void *ud;  // 协程参数
	ucontext_t ctx; // 协程上下文
	struct schedule * sdu; // 该协程所属的调度器
	ptrdiff_t cap; 	 // 已经分配的内存大小
	ptrdiff_t size; // 当前协程运行时栈,保存起来后的大小
	int status;	// 协程当前的状态
	char *stack; // 当前协程的保存起来的运行时栈

	coroutine(schedule* sdu,coroutine_func func, void *ud);
	~coroutine();
};

coroutine::coroutine(schedule* sdu,coroutine_func func, void *ud)
					:func(func),ud(ud),sdu(sdu),cap(0),
					size(0),status(static_cast<int>(STATE::COROUTINE_READY)),
					stack(nullptr)
{

}

coroutine::~coroutine(){
	free(stack);
}


Fiber::Fiber():sdu(make_shared<schedule>()){

}

// void Fiber::coroutine_close(struct schedule* sdu){

// }


int Fiber::coroutine_new(coroutine_func fun, void *ud){
	fiberPtr ptr = make_unique<coroutine>(sdu.get(),fun,ud);
	// 下标i当id
	for(int i = 0;i < sdu->co.size();++i){
		if(sdu->co[i] == nullptr){
			sdu->co[i] = std::move(ptr);
			return i;
		}
	}
	// 上面没返回的话走这步
	sdu->co.emplace_back(std::move(ptr));
	return sdu->co.size() - 1;
}

void Fiber::mainfunc(uint32_t low32, uint32_t hi32) {
	uintptr_t ptr = (uintptr_t)low32 | ((uintptr_t)hi32 << 32);
	struct schedule* sdu = (struct schedule *)ptr;
	int id = sdu->running;
	auto co = sdu->co[id];
	// 调用完就关闭
	co->func(co->ud);// 中间可能会被切出去
	co.reset();
	sdu->co[id] = nullptr;
	sdu->running = -1;
}	

// 运行/切换协程
void Fiber::coroutine_resume(int id){
	assert(sdu->running == -1);
	assert(id >=0);

	fiberPtr co = sdu->co[id];
	if(co == nullptr)return ;
	int statu = co->status;
	switch (statu)
	{
		case static_cast<int>(STATE::COROUTINE_READY) :
		{
			getcontext(&co->ctx);
			co->ctx.uc_stack.ss_sp = sdu->stack;
			co->ctx.uc_stack.ss_size = STACK_SIZE;
			co->ctx.uc_link = &sdu->main;
			
			sdu->running = id;
			co->status = static_cast<int>(STATE::COROUTINE_RUNNING);

			uintptr_t ptr = (uintptr_t)sdu.get();

			makecontext(&co->ctx,(void (*)(void))&Fiber::mainfunc,2,(uint32_t)ptr, (uint32_t)(ptr>>32)); 
			swapcontext(&sdu->main,&co->ctx);
			break;
		}
		case static_cast<int>(STATE::COROUTINE_SUSPEND) : {
			// 相当于把值写在了栈顶,放在调度器上面
			memcpy(sdu->stack + STACK_SIZE - co->size,co->stack,co->size);
			sdu->running = id;
			co->status = static_cast<int>(STATE::COROUTINE_RUNNING);
			swapcontext(&sdu->main,&co->ctx);
			break;
		}
		default:
			assert(0);
			return ;
	}
}


int Fiber::coroutine_status(int id){
	assert(id >= 0 && id < sdu->co.size());
	if(sdu->co[id] == nullptr){
		return static_cast<int>(STATE::COROUTINE_DEAD);
	}
	return sdu->co[id]->status;
}
static void _save_stack(coroutine* co, char *top) {
	char dumy = 0;
	assert(top - &dumy <= STACK_SIZE);
	if(co->cap < top - &dumy){
		free(co->stack);
		co->cap = top - &dumy;
		co->stack = static_cast<char*>(malloc(co->cap));
	}
	co->size = top - &dumy;
	memcpy(co->stack,&dumy,co->size);
}
void Fiber::coroutine_yield(){
	int id = sdu->running;
	assert(id >= 0);

	auto co = sdu->co[id];
	_save_stack(co.get(),sdu->stack + STACK_SIZE);

	sdu->running = -1;
	co->status = static_cast<int>(STATE::COROUTINE_SUSPEND);

	swapcontext(&co->ctx,&sdu->main);
}
int Fiber::coroutine_running(){
	return sdu->running;
}

test.cpp

#include "fiber.h"
#include <stdio.h>
#include <memory>

struct args {
	int n;
};
int N = 0;
Fiber* fiber;

constexpr int SIZE = 10000000;

static void foo(void *ud) {
	struct args * arg = (args*)ud;
	int start = arg->n;
	int i;
	for (i=0;i<SIZE;i++) {
		printf("coroutine %d : %d\n",fiber->coroutine_running() , ++arg->n);
		fiber->coroutine_yield();
	}
}

static void test() {
	struct args arg1 = { 0 };
	struct args arg2 = { 100 };

	int co1 = fiber->coroutine_new(foo, &arg1);
	int co2 = fiber->coroutine_new(foo, &arg2);
	printf("main start\n");
	while (fiber->coroutine_status(co1) && fiber->coroutine_status(co2)) {
		fiber->coroutine_resume(co1);
		fiber->coroutine_resume(co2);
	} 
	printf("arg1 : %d args2 : %d\n",arg1.n,arg2.n);
	printf("main end\n");
}

int main() {
    fiber = new Fiber();
	test();
	
    delete fiber;
	return 0;
}


我更倾向每根协程一个栈。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值