`写了一个简易的协程库,取名为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;
}
我更倾向每根协程一个栈。