C/C++协程学习笔记丨C/C++实现协程及原理分析视频( 二 )


  • libco , Boost.context:基于汇编代码的上下文切换
  • phxrpc:基于 ucontext/Boost.context 的上下文切换
  • libmill:基于 setjump/longjump 的协程切换
一般而言 , 基于汇编的上下文切换要比采用系统调用的切换更加高效 , 这也是为什么 phxrpc 在使用 Boost.context 时要比使用 ucontext 性能更好的原因 。 关于 phxrpc 和 libmill 具体的协程实现方式 , 以后有时间再详细介绍 。
libco 协程的创建和切换在介绍 coroutine 的创建之前 , 我们先来熟悉一下 libco 中用来表示一个 coroutine 的数据结构 , 即定义在 co_routine_inner.h 中的 stCoRoutine_t:
struct stCoRoutine_t
{
stCoRoutineEnv_t *env; // 协程运行环境
pfn_co_routine_t pfn; // 协程执行的逻辑函数
void *arg; // 函数参数
coctx_t ctx; // 保存协程的下文环境
...
char cEnableSysHook; // 是否运行系统 hook , 即非侵入式逻辑
char cIsShareStack; // 是否在共享栈模式
void *pvEnv;
stStackMem_t* stack_mem; // 协程运行时的栈空间
char* stack_sp; // 用来保存协程运行时的栈空间
unsigned int save_size;
char* save_buffer;
};
我们暂时只需要了解表示协程的最简单的几个参数 , 例如协程运行环境 , 协程的上下文环境 , 协程运行的函数以及运行时栈空间 。 后面的 stack_sp , save_size 和 save_buffer 与 libco 共享栈模式相关 , 有关共享栈的内容我们后续再说
协程创建和运行由于多个协程运行于一个线程内部的 , 因此当创建线程中的第一个协程时 , 需要初始化该协程所在的环境 stCoRoutineEnv_t , 这个环境是线程用来管理协程的 , 通过该环境 , 线程可以得知当前一共创建了多少个协程 , 当前正在运行哪一个协程 , 当前应当如何调度协程:
struct stCoRoutineEnv_t
{
stCoRoutine_t *pCallStack[ 128 ]; // 记录当前创建的协程
int iCallStackSize; // 记录当前一共创建了多少个协程
stCoEpoll_t *pEpoll; // 该线程的协程调度器
// 在使用共享栈模式拷贝栈内存时记录相应的 coroutine
stCoRoutine_t* pending_co;
stCoRoutine_t* occupy_co;
};
上述代码表明 libco 允许一个线程内最多创建 128 个协程 , 其中 pCallStack[iCallStackSize-1] 也就是栈顶的协程表示当前正在运行的协程 。 当调用函数 co_create 时 , 首先检查当前线程中的 coroutine env 结构是否创建 。 这里 libco 对于每个线程内的 stCoRoutineEnv_t 并没有使用 thread-local 的方式(例如gcc 内置的 __thread , phxrpc采用这种方式)来管理 , 而是预先定义了一个大的数组 , 并通过对应的 PID 来获取其协程环境 。 :
static stCoRoutineEnv_t* g_arrCoEnvPerThread[204800]
stCoRoutineEnv_t *co_get_curr_thread_env()
{
return g_arrCoEnvPerThread[ GetPid() ];
}
初始化 stCoRoutineEnv_t 时主要完成以下几步:
  1. 为 stCoRoutineEnv_t 申请空间并且进行初始化 , 设置协程调度器 pEpoll 。
  2. 创建一个空的 coroutine , 初始化其上下文环境( 有关 coctx 在后文详细介绍 ) , 将其加入到该线程的协程环境中进行管理 , 并且设置其为 main coroutine 。 这个 main coroutine 用来运行该线程主逻辑 。
当初始化完成协程环境之后 , 调用函数 co_create_env 来创建具体的协程 , 该函数初始化一个协程结构 stCoRoutine_t , 设置该结构中的各项字段 , 例如运行的函数 pfn , 运行时的栈地址等等 。 需要说明的就是 , 如果使用了非共享栈模式 , 则需要为该协程单独申请栈空间 , 否则从共享栈中申请空间 。 栈空间表示如下:
struct stStackMem_t
{
stCoRoutine_t* occupy_co; // 使用该栈的协程
int stack_size; // 栈大小
char* stack_bp; // 栈的指针 , 栈从高地址向低地址增长
char* stack_buffer; // 栈底
};
使用 co_create 创建完一个协程之后 , 将调用 co_resume 来将该协程激活运行:
void co_resume( stCoRoutine_t *co )
{
stCoRoutineEnv_t *env = co->env;
// 获取当前正在运行的协程的结构
stCoRoutine_t *lpCurrRoutine = env->pCallStack[ env->iCallStackSize - 1 ];
if( !co->cStart )
{
// 为将要运行的 co 布置上下文环境
coctx_make(
co->cStart = 1;
}
env->pCallStack[ env->iCallStackSize++ ] = co; // 设置co为运行的线程
co_swap( lpCurrRoutine, co );
}
函数 co_swap 的作用类似于 Unix 提供的函数 swapcontext:将当前正在运行的 coroutine 的上下文以及状态保存到结构 lpCurrRoutine 中 , 并且将 co 设置成为要运行的协程 , 从而实现协程的切换 。 co_swap 具体完成三项工作:
  1. 记录当前协程 curr 的运行栈的栈顶指针 , 通过 char c; curr_stack_sp= --tt-darkmode-color: #999999;">对应于 co_resume 函数 , 协程主动让出执行权则调用 co_yield 函数 。 co_yield 函数调用了 co_yield_env , 将当前协程与当前线程中记录的其他协程进行切换: