C++实现轻量级协程库

C++ 实现轻量级协程库

context

协程的实现与线程的主动切换有关,当当前上下文可能阻塞时,需要主动切换到其它上下文来避免操作系统将当前线程挂起从而降低效率。

在Linux中定义了ucontext_t结构体来表示线程的上下文结构。

typedef struct ucontext_t {
  struct ucontext_t *uc_link;//表示当当前上下文阻塞时会被切换的上下文。
  sigset_t           uc_sigmask;//被当前线程屏蔽的信号
  stack_t 					 uc_stack;//线程栈
  mcontext_t 				 uc_mcontext;//与机器相关的线程上下文的表示
} ucontext_t;

与上下文相关的有四个函数:

  • getcontext(ucontext_t* ucp): 调用后基于当前上下文初始化ucp所指向的上下文结构体。
  • setcontext(const ucontext_t* ucp): 切换到ucp所指向的上下文,如果调用成功则不会返回,因为上下文已经被切换。
  • makecontext(ucontext_t* ucp, void (*func)(), int argc, ...): 用于指定上下文需要执行的函数,要求在调用之前context已经确定栈和 uc_link. 当切换到该上下文后,函数func就会被执行。函数返回后,后继线程就会被切换到,如果uc_link为NULL,则线程退出。
  • swapcontext(ucontext_t* restrict oucp, const ucontext_t* restrict ucp): 将当前上下文保存到oucp中,然后切换到ucp对应的上下文中。与setcontext的区别在于是否保存当前上下文。

附上stack_t的定义:

typedef struct {
  void* ss_sp;
  int ss_flags;
  size_t ss_size;
} stack_t;

比如可以通过下面的程序实现循环打印:

#include <ucontext.h>
#include <unistd.h>
#include <stdio.h>

int main() {
    int idx = 0;
    ucontext_t ctx1;
    getcontext(&ctx1);

    printf("%d\n", idx);
    idx++;
    sleep(1);
    setcontext(&ctx1);
    return 0;
}

接下来以XFiber为例讲解一下一个轻量级协程库的基本实现方式。

包装上下文

Linux提供的协程结构体比较简陋,并不足以供协程库使用,因此需要进行包装一下。

struct Fiber {
  uint64_t seq_;
  XFiber* xfiber_;
  std::string fiber_name_;
  ucontext_t ctx_;
  uint8_t* stack_ptr_;
  size_t stack_size_;
  std::function<void()> run_;
  WaitingEvents waiting_events_;
};

其中XFiber为协程的调度器,后面会讲。WatingEvents为协程所需要等待的读和写的文件描述符。

struct WaitingEvents {
  std::vector<int> waiting_fds_w_;
  std::vector<int> waiting_fds_r_;
  int64_t expire_at_;
};

XFiber 上下文调度器

CreateFiber

先从最基本的创建一个协程开始,首先注意协程和线程的区别,协程代表一段可以分开执行的逻辑,但是和其它协程还是保持串行执行,因此协程创建并不会马上执行,而是由协程调度器统一执行。

先看看创建协程函数的签名:

void XFiber::CreateFiber(std::function<void()> run, size_t stack_size, std::string fiber_name);

run即要执行的函数,这里作者设定了只能是无参数、无返回值的函数类型,但是其实可以借助C++模板实现各种类型函数的注册。

协程调度器主要维护两个协程队列,分别是运行队列和就绪队列,运行队列中的协程会被切换到,而就绪队列中的协程会在下一次的循环中被切换到。

同时维护两个map,io_waiting_fibers_表示监听的文件描述符所对应的一对读和写的协程,expire_fibers_的value为一个有序集合,表示在某个时间点会超时的协程集合。

Dispatch

当Dispatch函数开始运行时,各协程才开始运行。该函数主要分为三个部分:

  • 处理已经就绪的协程。将就绪队列move到运行队列中,然后将就绪队列清空,这样做的原因是这一循环的就绪队列在运行中可能重新回到就绪队列中(主动Yield就会回到就绪队列)。

    协程切换过程就涉及到上面的swapcontext函数,为了使得协程能够在返回后能够重新回到XFiber中,其结构体中维护了一个sched_ctx_成员表示调度器的上下文。因此每一条Fiber在被创建时都将sched_ctx_作为接下来切换到的上下文,这样就保证了每一条协程在执行完成以后都能过回到调度器来,并由调度器处理接下来的就绪协程。

  • 检查超时的协程,对于超时的协程集合,需要将这些协程通过WakeupFiber函数进行唤醒。

  • 调用epoll相关方法,检查所有的epoll事件,并唤醒相关协程。

posted @ 2022-06-17 20:34  kaleidopink  阅读(738)  评论(0编辑  收藏  举报