基于ucontext的C++协程

基于ucontext的协程

ucontext是glibc下的组件,用来管理程序执行的上下文,重点是四个函数:

1
2
3
4
5
#include <ucontext.h>
int getcontext(ucontext_t *ucp);
int setcontext(const ucontext_t *ucp);
void makecontext(ucontext_t *ucp, void (*func)(),int argc, ...);
int swapcontext(ucontext_t *oucp, const ucontext_t *ucp);

在使用ucontext封装c++风格的轻量级协程的过程中,主要是干下面几件事:

  1. 对调度器、协程的资源初始化。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    class Coroutine{
    public:
    Coroutine(coroutineFunc func, void* args, CoScheduler* sche);
    ~Coroutine();

    void saveStack(char* top);
    void dofunc();
    int getStatus(){
    return status_;
    }
    void setId(int id){
    id_ = id;
    }

    public:
    coroutineFunc func_;
    void* args_;
    CoScheduler* sehcduler_;
    ptrdiff_t capacity_;
    ptrdiff_t curSize_;
    ucontext_t ctx_;
    CoStatus status_;
    char* stack_;
    int id_;
    };
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    class CoScheduler{
    public:
    CoScheduler();
    ~CoScheduler();
    int createCoroutine(coroutineFunc func, void* args);
    void deleteCoroutine(int id);
    void resume(int id);
    void yield();
    int currentCo(){
    return runningCo_;
    }
    int getStatus(int id){
    assert(id>=0 && id < capacity_);
    if (coroutines_[id] == nullptr) {
    return COURTINUE_DEAD;
    }
    return coroutines_[id] -> status_;
    }
    public:
    CoScheduler(const CoScheduler&) = delete;
    CoScheduler& operator=(const CoScheduler&) = delete;
    char stack_[STACK_SIZE];
    ucontext_t mainCtx_;
    int numCoroutines_;
    int capacity_;
    int runningCo_;
    std::vector<Coroutine*> coroutines_;
    };
  2. 对调度器调度方式实现。

这里主要有对称和非对称之别,主要区别:

  • 非对称:类似于函数调用,某个协程切换到另一个协程,最终是要返回到本协程调用处的。在实现基于ucontext的协程的过程中,采用的是非对称方式,主要是因为非对称的方式比较好控制。
  • 对称则不存在这种关系,程序执行权在协程之间任意切换。例如go的协程。
  1. 协程堆栈的封装

在有栈协程里,对协程堆栈的实现也分为两种,一种是共享堆栈,一种是独立栈。

  • 独立栈:每个协程都有自己的堆栈数据,每次执行之后保存,优点是切换速度快,缺点是需要分配足够大的内存,否则运行过程会导致栈溢出。
  • 共享栈:在主协程分配足够大的栈空间,每次执行将协程堆栈拷贝到此空间,缺点是拷贝耗时,优点是省空间。

除此之外,由于ucontext有浮点数上下文和sigmask(信号屏蔽掩码)的保存,因此切换的效率并不高,在一些工业级的应用场景下,常常自定义汇编代码进行上下文切换,如libco,接下来我会参考libco来使用x86汇编实现一个小型协程库。