服务器之家:专注于服务器技术及软件下载分享
分类导航

服务器资讯|IT/互联网|云计算|区块链|软件资讯|操作系统|手机数码|百科知识|免费资源|头条新闻|

服务器之家 - 新闻资讯 - 服务器资讯 - Tars-Cpp 协程实现分析

Tars-Cpp 协程实现分析

2023-05-06 18:59未知服务器之家 服务器资讯

一、前言 Tars 是 Linux 基金会的开源项目 (​​https://github.com/TarsCloud​​),它是基于名字服务使用 Tars 协议的高性能 RPC 开发框架,配套一体化的运营管理平台,并通过伸缩调度,实现运维半托管服务。Tars 集可扩展协议编解码、

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"),协程认为是“可以暂停和恢复执行”的函数。

Tars-Cpp 协程实现分析

协程可以看成一种特殊的函数,相比于函数,协程最大的特点就是支持挂起(yield)和恢复(resume)的能力。如上图所示:函数不能主动中断执行流;而协程支持主动挂起,中断执行流,并在一定时机恢复执行。

协程的作用:

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

2.2 进程、线程和协程

我们可以简单的认为协程是用户态的线程。协程和线程主要异同:

  1. 相同点:都可以实现上下文切换(保存和恢复执行流)
  2. 不同点:线程的上下文切换在内核实现,切换的时机由内核调度器控制。协程的上下文切换在用户态实现,切换的时机由调用方自身控制。

进程、线程和协程的比较:

Tars-Cpp 协程实现分析

2.3 协程的分类

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

  • 对称协程:协程之间相互独立,调度权(CPU)可以在任意协程之间转移。协程只有一种控制传递操作(yield)。对称协程一般需要调度器支持,通过调度算法选择下一个目标协程。
  • 非对称协程:协程之间存在调用关系,协程让出的调度权只能返回给调用者。协程有两种控制操作:恢复(resume)和挂起(yield)。

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

Tars-Cpp 协程实现分析

对称协程示意图

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

Tars-Cpp 协程实现分析

非对称协程示意图

根据协程是否有独立的栈空间,协程分为有栈协程(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)的能力,即可以中断自己的执行流,并且在合适的时候恢复执行流,这也称为上下文切换的能力。

协程执行的过程,依赖两个关键要素:协程栈和寄存器,协程的上下文环境其实就是寄存器和栈的状态。实现上下文切换的核心就是实现保存并恢复当前执行环境的寄存器状态的能力。

实现用户态上下文切换一般有以下方式:

Tars-Cpp 协程实现分析

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);

延伸 · 阅读

精彩推荐