微信开源C++Libco介绍与应用(二)

本文详细解析了微信开源的 C++ 协程库 Libco,涵盖了协程的生命周期,包括创建、启动、挂起和恢复。文中介绍了 `co_create` 和 `co_resume` 函数,阐述了非对称协程的 `co_yield` 操作,以及上下文切换的关键函数 `coctx_swap`。此外,文章还探讨了 Libco 中的事件驱动和协程调度,强调了主协程在事件循环中的作用,以及如何通过 `epoll` 和时间轮实现高效的定时器。
摘要由CSDN通过智能技术生成

上文我们介绍了Libco的初步介绍与协程,接下来让我们继续往下看~


Libco 协程的生命周期

创建协程(Creating coroutines)

前文已提到,libco 中创建协程是 co_create() 函数。函数声明如下:

int co_create(stCoRoutine_t** co, const stCoRoutineAttr_t* attr, void* (*routine)(void*), void* arg);

同 pthread_create 一样,该函数有四个参数:

  • @co: stCoRoutine_t** 类型的指针。输出参数,co_create 内部会为新协程分配一个“协程控制块”,*co 将指向这个分配的协程控制块。
  • @attr: stCoRoutineAttr_t* 类型的指针。输入参数,用于指定要创建协程的属性,可为 NULL。实际上仅有两个属性:栈大小、指向共享栈的指针(使用共享栈模式)。
  • @routine: void* (*)(void *) 类型的函数指针,指向协程的任务函数,即启动这个协程后要完成什么样的任务。routine 类型为函数指针。
  • @arg: void* 类型指针,传递给任务函数的参数,类似于 pthread 传递给线程的参数。

调用 co_create 将协程创建出来后,这时候它还没有启动,也即是说我们传递的 routine 函数还没有被调用。实质上,这个函数内部仅仅是分配并初始化 stCoRoutine_t 结构体、设置任务函数指针、分配一段“栈”内存,以及分配和初始化 coctx_t。

为什么这里的“栈”要加个引号呢?因为这里的栈内存,无论是使用预先分配的共享栈,还是 co_create 内部单独分配的栈,其实都是调用 malloc 从进程的堆内存分配出来的。对于协程而言,这就是“栈”,而对于底层的进程(线程)来说这只不过是普通的堆内存而已。

总体上,co_create 函数内部做的工作很简单,这里就不贴出代码了。


启动协程(Resume a coroutine)

在调用 co_create 创建协程返回成功后,便可以调用 co_resume 函数将它启动了。该函数声明如下:

void co_resume(stCoRoutine_t* co);

它的意义很明了,即启动 co 指针指向的协程。

值得注意的是,为什么这个函数不叫 co_start 而是 co_resume 呢?前文已提到,libco 的协程是非对称协程,协程在让出 CPU 后要恢复执行的时候,还是要再次调用一下 co_resume 这个函数的去“启动”协程运行的。从语义上来讲,co_start 只有一次,而 co_resume 可以是暂停之后恢复启动,可以多次调用,就这么个区别。

实际上,看早期关于协程的文献,讲到非对称协程,一般也都用 “resume” 与 “yield” 这两个术语。协程要获得 CPU 执行权用 “resume”,而让出 CPU 执行权用 “yield”,这是两个是两个不同的(不对称的)过程,因此这种机制才被称为非对称协程(asymmetric coroutines)。

所以讲到 resume 一个协程,我们一定得注意,这可能是第一次启动该协程,也可以是要准备重新运行挂起的协程。我们可以认为在 libco 里面协程只有两种状态,即 running 和 pending 。当创建一个协程并调用 resume 之后便进入了 running 状态,之后协程可能通过 yield 让出 CPU,这就进入了 pending 状态。不断在这两个状态间循环往复,直到协程退出(执行的任务函数 routine 返回),如图2所示。


v2-13c5651ee1eac032afb68d9929e4df94_b.jpg


需要指出的是,不同于 Go 语言,这里 co_resume() 启动一个协程的含义,不是“创建一个并发任务”。进入 co_resume() 函数后发生协程的上下文切换,协程的任务函数是立即就会被执行的,而且这个执行过程不是并发的(Concurrent)。

为什么不是并发的呢?因为 co_resume() 函数内部会调用 coctx_swap() 将当前协程挂起,然后就开始执行目标协程的代码了(具体过程见下文协程切换那一节的分析)。本质上这个过程是串行的,在一个操作系统线程(进程)上发生的,甚至可以说在一颗 CPU 核上发生的(假定没有发生 CPU migration)。

让我们站到 Knuth 的角度,将 coroutine 当做一种特殊的 subroutine 来看,问题会显得更清楚:A 协程调用 co_resume(B) 启动了 B 协程,本质上是一种特殊的过程调用关系,A 调用 B 进入了 B 过程内部,这很显然是一种串行执行的关系。那么,既然 co_resume() 调用后进入了被调协程执行控制流,那么 co_resume() 函数本身何时返回?这就要等被调协程主动让出 CPU了。(TDB 补充图)


co_resume() 函数代码实现

void co_resume(stCoRoutine_t *co) {
    stCoRoutineEnv_t *env = co->env;
    stCoRoutine_t *lpCurrRoutine = env->pCallStack[env->iCallStackSize-1];
    if (!co->cStart) {
        coctx_make(&co->ctx, (coctx_pfn_t)CoRoutineFunc, co, 0);
        co->cStart = 1;
    }
    env->pCallStack[env->iCallStackSize++] = co;
    co_swap(lpCurrRoutine, co);
}


如果读者对 co_resume() 的逻辑还有疑问,不妨再看一下它的代码实现。

第5、6行的if条件分支,当且仅当协程是第一次启动时才会执行到。首次启动协程过程有点特殊,需要调用 coctx_make() 为新协程准备 context(为了让 co_swap() 内能跳转到协程的任务函数),并将 cStart 标志变量置 1 。

忽略第4~7行首次启动协程的特殊逻辑,那么 co_resume() 仅有 4 行代码而已。第 3 行取当前协程控制块指针,第 8 行将待启动的协程 co 压入 pCallStack 栈,然后第 9 行就调用 co_swap() 切换到 co 指向的新协程上去执行了。前文也已经提到,co_swap() 不会就此返回,而是要这次 resume 的 co 协程主动 yield 让出 CPU 时才会返回到 co_resume() 中来。

值得指出的是,这里讲 co_swap() 不会就此返回,不是说这个函数就阻塞在这里等待 co 这个协程 yield 让出 CPU。实际上,后面我们将会看到,co_swap() 内部已经切换了 CPU 执行上下文,奔着 co 协程的代码路径去执行了。整个过程不是并发的,而是串行的,这一点我们已经反复强调过了。


协程的挂起(Yield to another coroutine)

在非对称协程理论,yield 与 resume 是个相对的操作。A 协程 resume 启动了 B 协程,那么只有当 B 协程执行 yield 操作时才会返回到 A 协程。在上一节剖析协程启动函数 co_resume() 时,也提到了该函数内部 co_swap() 会执行被调协程的代码。只有被调协程 yield 让出 CPU,调用者协程的 co_swap() 函数才能返回到原点,即返回到原来 co_resume() 内的位置。

在前文解释 stCoRoutineEnv_t 结构 pCallStack 这个“调用栈”的时候,我们已经简要地提到了 yield 操作的内部逻辑。在被调协程要让出 CPU 时,会将它的 stCoRoutine_t 从 pCallStack 弹出,“栈指针” iCallStackSize 减 1,然后 co_swap() 切换 CPU 上下文到原来被挂起的调用者协程恢复执行。这里“被挂起的调用者协程”,即是调用者 co_resume() 中切换 CPU 上下文被挂起的那个协程。下面我们来看一下 co_yield_env() 函数代码:


co_yield_env() 函数

void co_yield_env(stCoRoutineEnv_t *env) {
    stCoRoutine_t *last = env->pCallStack[env->iCallStackSize - 2];
    stCoRoutine_t 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值