关于tars:TarsCpp-协程实现分析

2次阅读

共计 7063 个字符,预计需要花费 18 分钟才能阅读完成。

作者:vivo 互联网服务器团队 - Ye Feng

本文介绍了协程的概念,并探讨了 Tars Cpp 协程的实现原理和源码剖析。

一、前言

Tars 是 Linux 基金会的开源我的项目(https://github.com/TarsCloud),它是基于名字服务应用 Tars 协定的高性能 RPC 开发框架,配套一体化的经营治理平台,并通过伸缩调度,实现运维半托管服务。Tars 集可扩大协定编解码、高性能 RPC 通信框架、名字路由与发现、公布监控、日志统计、配置管理等于一体,通过它能够疾速用微服务的形式构建本人的稳固牢靠的分布式应用,并实现残缺无效的服务治理。

Tars 目前反对 C++,Java,PHP,Nodejs,Go 语言,其中 TarsCpp 3.x 全面启用对协程的反对,服务框架全面交融协程。本文基于 TarsCpp-v3.0.0 版本,探讨了协程在 TarsCpp 服务框架的实现。

二、协程的介绍

2.1 什么是协程

协程的概念最早呈现在 Melvin Conway 在 1963 年的论文(”Design of a separable transition-diagram compiler”),协程认为是“能够暂停和复原执行”的函数。

协程能够看成一种非凡的函数,相比于函数,协程最大的特点就是反对挂起(yield)和复原(resume)的能力。如上图所示:函数不能被动中断执行流;而协程反对被动挂起,中断执行流,并在肯定机会复原执行。

协程的作用

  1. 升高并发编码的复杂度,尤其是异步编程(callback hell)。
  2. 协程在用户态中实现调度,防止了陷入内核,上下文切换开销小。

2.2 过程、线程和协程

咱们能够简略的认为协程是用户态的线程。协程和线程次要异同:

  1. 相同点:都能够实现上下文切换(保留和复原执行流)
  2. 不同点:线程的上下文切换在内核实现,切换的机会由内核调度器管制。协程的上下文切换在用户态实现,切换的机会由调用方本身管制。

过程、线程和协程的比拟:

2.3 协程的分类

按管制传递(Control-transfer)机制分为:对称(Symmetric)协程和非对称(Asymmetric)协程。

  • 对称协程:协程之间互相独立,调度权(CPU)能够在任意协程之间转移。协程只有一种管制传递操作(yield)。对称协程个别须要调度器反对,通过调度算法抉择下一个指标协程。
  • 非对称协程:协程之间存在调用关系,协程让出的调度权只能返回给调用者。协程有两种管制操作:复原(resume)和挂起(yield)。

下图演示了对称协程的调度权转移流程,协程只有一个操作 yield,示意让出 CPU,返回给调度器。

下图演示了非对称协程的调度权转移流程。协程能够有两个操作,即 resume 和 yield。resume 示意转移 CPU 给被调用者,yield 示意被调用者返回 CPU 给调用者。

依据协程是否有独立的栈空间,协程分为有栈协程(stackful)和无栈协程(stackless)两种。

  • 有栈协程:每个协程有独立的栈空间,保留独立的上下文(执行栈、寄存器等),协程的唤醒和挂起就是拷贝和切换上下文。长处:协程调度能够嵌套,在内存中的任意地位、任意时刻进行。局限:协程数目增大,内存开销增大。
  • 无栈协程:单个线程内所有协程都共享同一个栈空间(共享栈),协程的切换就是简略的函数调用和返回,无栈协程通常是基于状态机或闭包来实现。长处:减小内存开销。局限:协程调度产生的局部变量都在共享栈上, 一旦新的协程运行后共享栈中的数据就会被笼罩, 先前协程的局部变量也就不再无效, 进而无奈实现参数传递、嵌套调用等高级协程交互。

Golang 中的 goroutine、Lua 中的协程都是有栈协程;ES6 的 await/async、Python 的 Generator、C++20 中的 cooroutine 都是无栈协程。

三、Tars 协程实现

实现协程的外围有两点:

  • 实现用户态的上下文切换。
  • 实现协程的调度。

Tars 协程的由上面几个类实现

  • TC_CoroutineInfo 协程信息类:实现协程的上下文切换。每个协程对应一个 TC_CoroutineInfo 对象,上下文切换基于 boost.context 实现。
  • TC_CoroutineScheduler 协程调度器类:实现了协程的治理和调度。
  • TC_Coroutine 协程类:继承于线程类(TC_Thread),不便业务疾速应用协程。

Tars 协程有几个特点:

  • 有栈协程。每个协程都调配了独立的栈空间。
  • 对称协程。协程之间互相独立,由调度器负责调度。
  • 基于 epoll 实现协程调度,和网络 IO 无缝联合。

3.1 用户态上下文切换的实现形式

协程能够看成一种非凡的函数,和一般函数不同,协程函数有挂起 (yield) 和复原 (resume) 的能力,即能够中断本人的执行流,并且在适合的时候复原执行流,这也称为上下文切换的能力。

协程执行的过程,依赖两个要害因素:协程栈和寄存器,协程的上下文环境其实就是寄存器和栈的状态。实现上下文切换的外围就是实现保留并复原以后执行环境的寄存器状态的能力。

实现用户态上下文切换个别有以下形式:

3.2  基于 boost.context 实现上下文切换

Tars 协程是基于 boost.context 实现,boost.context 提供了两个接口(make_fcontext, jump_fcontext)实现协程的上下文切换。

代码 1:

/**
 * @biref 执行环境上下文
 */
typedef void*   fcontext_t;

/**
 * @biref 事件参数包装
 */

struct transfer_t {
    fcontext_t     fctx; // 起源的执行上下文。起源的上下文指的是从什么地位跳转过来的
    void*      data; // 接口传入的自定义的指针
};

/**
 * @biref 初始化执行环境上下文
 * @param sp 栈空间地址
 * @param size 栈空间的大小
 * @param fn 入口函数
 * @return 返回初始化实现后的执行环境上下文
 */
extern "C" fcontext_t make_fcontext(void * stack, std::size_t stack_size, void (* fn)(transfer_t));

/**
 * @biref 跳转到指标上下文
 * @param to 指标上下文
 * @param vp 指标上下文的附加参数,会设置为 transfer_t 里的 data 成员
 * @return 跳转起源
 */
extern "C" transfer_t jump_fcontext(fcontext_t const to, void * vp);

(1)make_fcontext 创立协程

  • 承受三个参数,stack 是为协程调配的栈底,stack_size 是栈的大小,fn 是协程的入口函数
  • 返回初始化实现后的执行环境上下文

(2)jump_fcontext 切换协程

  • 承受两个参数,指标上下文地址和参数指针
  • 返回一个上下文,指向以后上下文从哪个上下文跳转过来

make_fcontext 和 jump_fcontext 通过汇编代码实现,具体的汇编代码能够参考:

  • https://github.com/TarsCloud/TarsCpp/blob/v3.0.0/util/src/asm/jump_x86_64_sysv_elf_gas.S
  • https://github.com/TarsCloud/TarsCpp/blob/v3.0.0/util/src/asm/make_x86_64_sysv_elf_gas.S

boost context 是通过 fcontext_t 构造体来保留协程状态。绝对于其它汇编实现的协程库,boost 的 context 和 stack 是一起的,栈底指针就是 context,切换 context 就是切换 stack。

3.3  Tars 协程信息类

TC_CoroutineInfo  协程信息类,包装了 boost.context 提供的接口,示意一个 TARS 协程。

其中,TC_CoroutineInfo::registerFunc 定义了协程的创立。

代码 2:

void TC_CoroutineInfo::registerFunc(const std::function<void ()>& callback)
{
    _callback           = callback;
    _init_func.coroFunc = TC_CoroutineInfo::corotineProc;
    _init_func.args     = this;

    fcontext_t ctx      = make_fcontext(_stack_ctx.sp, _stack_ctx.size,
                                TC_CoroutineInfo::corotineEntry); // 创立协程
    transfer_t tf       = jump_fcontext(ctx, this); // context 切换

    // 理论的 ctx
    this->setCtx(tf.fctx);
}



void TC_CoroutineInfo::corotineEntry(transfer_t tf)
{TC_CoroutineInfo * coro = static_cast< TC_CoroutineInfo * >(tf.data); // this
    auto    func  = coro->_init_func.coroFunc;
    void*   args = coro->_init_func.args;

    transfer_t t = jump_fcontext(tf.fctx, NULL);

    // 拿到本人的协程堆栈, 以后协程完结当前, 好跳转到 main
    coro->_scheduler->setMainCtx(t.fctx);

    // 再跳转到具体函数
    func(args, t);
}

TC_CoroutineInfo::switchCoro 定义了协程切换。

代码 3:

void TC_CoroutineScheduler::switchCoro(TC_CoroutineInfo *to)
{
    // 跳转到 to 协程
    _currentCoro = to;

    transfer_t t = jump_fcontext(to->getCtx(), NULL);

    // 并保留协程堆栈
    to->setCtx(t.fctx);
}

四、Tars 协程调度器

基于 boost.context 的 TC_CoroutineInfo 类实现了协程的上下文切换,协程的治理和调度,则是由 TC_CoroutineScheduler 协程调度器类来负责,分治理和调度两个方面来阐明 TC_CoroutineScheduler 调度类。

  • 协程治理:目标是须要正当的数据结构来组织协程(TC_CoroutineInfo),不便调度的实现。
  • 协程调度:目标是管制协程的启动、休眠和唤醒,实现了 yield, sleep 等性能,实质就是实现协程的状态机,实现协程的状态切换。Tars 协程分为 5 个状态:FREE, ACTIVE, AVAIL, INACTIVE, TIMEOUT

代码 4:

/**
     * 协程的状态信息
     */
    enum CORO_STATUS
    {
        CORO_FREE       = 0,
        CORO_ACTIVE     = 1,
        CORO_AVAIL      = 2,
        CORO_INACTIVE   = 3,
        CORO_TIMEOUT    = 4 
    };

4.1 Tars 协程的治理

TC_CoroutineScheduler 次要通过以下办法治理协程:

  1. TC_CoroutineScheduler::create() 创立 TC_CoroutineScheduler 对象
  2. TC_CoroutineScheduler::init() 初始化,调配协程栈内存
  3. TC_CoroutineScheduler::run() 启动调度
  4. TC_CoroutineScheduler::terminate() 进行调度
  5. TC_CoroutineScheduler::destroy() 资源销毁,开释协程栈内存

咱们能够通过 TC_CoroutineScheduler::init() 看到数据结构的初始化过程。

代码 5:

void TC_CoroutineScheduler::init()
{
    ... ....

    createCoroutineInfo(_poolSize); // _all_coro = new TC_CoroutineInfo*[_poolSize+1];

    TC_CoroutineInfo::CoroutineHeadInit(&_active);
    TC_CoroutineInfo::CoroutineHeadInit(&_avail);
    TC_CoroutineInfo::CoroutineHeadInit(&_inactive);
    TC_CoroutineInfo::CoroutineHeadInit(&_timeout);
    TC_CoroutineInfo::CoroutineHeadInit(&_free);

    int iSucc = 0;
    for(size_t i = 0; i < _currentSize; ++i)
    {
        //iId= 0 不应用, 给 mainCoro 应用!!!!
        uint32_t iId = generateId();
        stack_context s_ctx = stack_traits::allocate(_stackSize); // 调配协程栈内存
        TC_CoroutineInfo *coro = new TC_CoroutineInfo(this, iId, s_ctx);
        _all_coro[iId] = coro;
        TC_CoroutineInfo::CoroutineAddTail(coro, &_free);
        ++iSucc;
    }
    _currentSize = iSucc;

    _mainCoro.setUid(0);
    _mainCoro.setStatus(TC_CoroutineInfo::CORO_FREE);

    _currentCoro = &_mainCoro;
}

通过上面的 TC_CoroutineScheduler 调度类数据结构图,能够更分明的看到协程的组织形式:

Tars 调度类数据结构

  • 应用协程之前,须要在协程数组(_all_coro),创立指定数量的协程对象,并为每个协程调配协程栈内存。
  • 通过链表的形式治理协程,每个状态都有一个链表。协程状态切换,对应协程在不同状态链表的转移。

4.2 Tars 协程的调度

Tars 调度是基于 epoll 实现,在 epoll 循环里查看是否有须要执行的协程, 有则执行之, 没有则期待在 epoll 对象上, 直到有唤醒或者超时。应用 epoll 实现的益处是能够和网络 IO 无缝粘合, 当有数据发送 / 接管时, 唤醒 epoll 对象, 从而实现协程的切换。

Tars 协程调度的外围逻辑是:TC_CoroutineScheduler::run()

代码 6:

void TC_CoroutineScheduler::run()
{
    ... ...

    while(!_epoller->isTerminate())
    {if(_activeCoroQueue.empty() && TC_CoroutineInfo::CoroutineHeadEmpty(&_avail) && TC_CoroutineInfo::CoroutineHeadEmpty(&_active))
        {_epoller->done(1000); // epoll_wait(..., 1000ms) 先解决 epoll 的网络事件
        }

        // 唤醒须要激活的协程
        wakeup();

        // 唤醒 sleep 的协程
        wakeupbytimeout();

        // 唤醒 yield 的协程
        wakeupbyself();

        int iLoop = 100;
        // 执行 active 协程, 每次执行 100 个, 防止占满 cpu
        while(iLoop > 0 && !TC_CoroutineInfo::CoroutineHeadEmpty(&_active))
        {
            TC_CoroutineInfo *coro = _active._next;
            switchCoro(coro);
            --iLoop;
        }

        // 执行 available 协程, 每次执行 1 个
        if(!TC_CoroutineInfo::CoroutineHeadEmpty(&_avail))
        {
            TC_CoroutineInfo *coro = _avail._next;
            switchCoro(coro);
        }
    }

    ... ...
}

下图能够更分明得看到协程调度和状态转移的过程。

TC_CoroutineScheduler 提供了上面四种办法实现协程的调度:

(1)TC_CoroutineScheduler::go(): 启动协程。

(2)TC_CoroutineScheduler::yield(): 以后协程放弃继续执行。并提供了两种形式,反对不同的唤醒策略。

  • yield(true): 会主动唤醒(等到下次协程调度, 都会再激活以后线程)
  • yield(false): 不再主动唤醒, 除非本人调度该协程(比方 put 到调度器中)

(3)TC_CoroutineScheduler::sleep(): 以后协程休眠 iSleepTime 工夫(单位: 毫秒),而后会被唤醒继续执行。

(4)TC_CoroutineScheduler::put(): 放入须要唤醒的协程, 将协程放入到调度器中, 马上会被调度器调度。

五、总结

本文介绍了协程的概念,并探讨了 Tars Cpp 协程的实现原理和源码剖析。

TarsCpp 3.x 全面启用对协程的反对,本文的源码剖析是基于 TarsCpp-v3.0.0 版本

https://github.com/TarsCloud/TarsCpp/tree/release/3.0

正文完
 0