C++ 协程之协程介绍

产生背景

对于后台开发,我们有一个非常重要的问题即使用尽可能少的服务器资源处理海量的请求,除了我们在架构上做多机自动扩容外,我们还必须尽可能提高单机硬件的利用率(CPU利用率 + IO利用率)。

为了提高硬件的利用率往往我们采用三种技术路径:

  • 多线程
  • 异步 IO
  • 协程

协程和异步 IO以及多线程的对比

1. 多线程

多线程可以充分利用 CPU 的多核,实现真正的并行,它是操作系统的基础设施。但是线程是一个粗粒度、相对比较笨重的多任务的抽象机制,例如以下几点:

  1. 创建线程非常耗时
  2. 线程上下文切换非常耗时(要从用户态切换到内核态;保存当前线程执行环境;加载目标线程的运行环境;再从内核态切换回来等一系列操作)
  3. 每个线程会预先分配一个几 M 的调用栈
  4. 系统中最多只能同时运行数干个线程(当就绪线程个数大于 CPU 核数,操作系统大量的时间就会用来进行线程切换,性能急剧下降)

2. 异步IO

操作系统中有两种 IO:同步IO 和异步 IO(5种 I/O 模型)

  • 同步IO:发起 IO(比如 read)后,CPU 必须原地等待其结束,然后才能继续往下执行
  • 异步IO:发起 IO(比如 read)前,会先注册一个回调函数;发起 IO 后,CPU 无需原地等待其完成,而是继续往下执行;CPU 收到 IO 完成的中断信号后,将调用回调函数处理 IO 完成后的剩下的工作

即:同步 IO 的逻辑直观,但是性能较低,仅适用于少量 IO;异步 IO 性能好,但是逻辑复杂,适用于大规模并发 IO

PS:
CPU 执行速度是远大于 IO 响应速度的,若执行同步IO,到达某个函数(read 函数执行 IO 操作),这个函数执行 5 分钟,那么当前线程就必须原地等待 5 分钟(忙等待),才能执行后续代码,这对于 CPU 资源十分浪费。但是在异步 IO 模式下,CPU 和 IO 可以全速并行执行,使用率大大提高。

3. 协程

多线程和异步 IO 的缺点即是协程的优点

  • 协程的特点在于是一个线程执行,最大的优势就是协程极高的执行效率。因为创建或切换协程不是线程创建/切换,它相当于一次函数调用,由程序自身控制,和多线程比,线程数量越多,协程的性能优势就越明显。
  • 协程不需要多线程的锁机制,因为只有一个线程,页不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多
  • 每个协程通常只需要几十个字节保存相关状态信息,空间开销远低于线程栈;
  • 系统中可同时运行数千万个协程,数量主要取决于可用内存大小(当然 CPU 核数也限制)
  • 协程具有异步 IO 的性能,和同步 IO 的代码编写方式

PS:
协程不是进程或线程,其执行过程更类似于子例程,或者说不带返回值的函数调用,我们知道多个线程相对独立,有自己的上下文,切换受系统控制;而协程也相对独立,有自己的上下文,但是其切换由自己控制,由当前协程切换到其它协程由当前协程来控制。


处理 IO 密集型任务

对于大多数后台程序面临的都是 IO 密集型任务,它的处理流程如下:

  1. 侦听线程收到一个客户端请求
  2. 侦听线程把请求加入一个队列,然后继续侦听下一个请求
  3. 工作线程从队列中取出一个请求,然后发起 IO
  4. IO 完成后,向客户端返回结果

用以上三种方式实现这种架构

架构描述
多线程架构侦听线程收到一个客户端线程后,会创建一个新线程(或从线程池种申请一个空闲线程)处理该连接,工作线程发起同步 IO后,原地等待其完成;然后向客户端返回请求结果
异步 IO工作线程从队列中取出一个请求后,然后注册一个回调函数并发起一个异步 IO;然后立刻返回,周而复始的处理下一个请求;IO 完成后,由回调函数向客户端返回结果
协程架构工作线程从队列中取出一个请求后,调用一个处理协程;然后立即返回,周而复始的处理下一个请求;处理协程负责发起 IO;IO 完成后,由处理协程向客户端返回请求结果

三种架构的对比

架构特点
多线程逻辑直观,容易实现,但不能充分利用后台服务器的硬件资源,容易发生阻塞
异步 IO逻辑杂乱,维护麻烦,但性能卓越,目前依旧是后台服务器的主流架构
协程性能卓越且逻辑直观,是后台服务器架构的演进方向

协程的实现机制

协程和线程的实现很相似,每个线程都有一个对应的线程处理函数,每个协程也有一个对应的协程函数;线程函数和普通函数没有区别,但是协程函数和普通函数有区别。

  • 普通函数:每次调用只能从第一条语句开始执行
  • 协程函数:协程交出控制权后,可以再次从交出控制权的下一句语句开始执行(类比多线程的调度方式)

PS:
普通函数每次执行都会从入口进入,当 A 函数调用 B 函数,那么只有等待 B 函数执行完成才能去执行 A 函数剩下的代码,函数做不到当 B 执行一半时,再去调用 A,并且从 A 剩下代码开始执行(每次调用函数都是从函数入口重新开始)。但是协程就可以做到,这种方式和线程十分类似。因为调用普通函数时,调用方的返回地址、入口参数等信息都保存在栈上。函数返回后,栈上的信息会被自动清除,所以每次调用普通函数都只能从第一句开始。

协程函数的特点

  1. 首次调用协程函数,会从堆中分配一个协程上下文,调用方的返回地址、入口函数、交出控制权等信息保存在协程的上下文中
  2. 当协程中途交出控制权口,协程的上下文不会被自动删除(相当于函数退出后,上下文环境还被保存,类比线程切换)
  3. 当协程再次获得控制权后,会自动从协程的上下文中恢复调用环境,然后从上一次交出控制权的下一条语句继续执行(加载目标协程环境,类比线程切换)
  4. 协程函数返回(非中途交出控制权)后,协程上下文被删除
  5. 若再次调用协程函数,视为首次调用

上面所描述的是协程实现的一般原理,根据实现方式不同,可分为有栈协程和无栈协程


有栈协程

技术路线:一个线程可以创建大量协程,每个协程都会保留一个私有栈,协程一旦执行到异步 IO 处,就会主动交出控制权。同一线程的所有协程以串行方式协作执行,没有数据争用的问题。

有栈协程的特点

  1. 每个协程都有一个预先分配的调用栈(Call Stack)
  2. 每个协程都属于且仅属于创建它的线程
  3. 一个线程可以包含多个协程
  4. 线程本身也可以是一个协程,称为主协程(Primary Coroutine)
  5. 协程必须主动交出控制权,否则同一线程的其它协程均无法获得执行机会
  6. 协程执行路径上,任何被调用的函数均可在任何位置交出控制权
  7. 如果允许把控制权交给同一线程的其它协程,则称为对称协程(Symm-etry Coroutines)。如果只允许把控制权交给主协程,主协程作为调度器,负责分配执行权,则称为非对称协程(Asymmetry Coroutines)
  8. 属于同一线程的多个协程之间没有数据争用(Data Race)问题
  9. 无需修改语言标准和编译器,利用系统调用即可实现

PS1:为了减少有栈协程的空间开销,有些协程框架用一个共享栈代替每个协程的私有栈。共享栈虽然降低了协程的空间开销,但却引进了栈拷贝的时间开销。
PS2:对称协程调度逻辑复杂,应用的场景有限,非对称协程是有栈协程的主流

有栈协程可通过操作系统提供的系统调用实现

OS系统调用
Linuxgetcontext,setcontext,makecontext,swapcontext
WindowsCreateFiber,ConvertFiberToThread,SwitchToFiber

无栈协程

技术路线:将异步 IO 封装到协程函数中,协程函数发起异步 IO 后,立即保存执行环境,然后把控制权交给调用方(Caller),调用方继续往下执行;异步 IO 完成后,负责处理 IO 完成事件的回调函数获得控制权,回调函数再把控制权转交给发起 IO 的协程,发起 IO 的协程首先恢复其执行环境,然后从上一次交出控制权的下一条语句继续往下执行

无栈协程的特点

  1. 每个协程的执行环境,仅需包含调用栈(Call Stack)的顶层栈帧(Top Call Stack Frame),而非整个调用栈,因而空间开销极小
  2. 协程执行路径上,只有特定语句才能交出控制权
  3. 无需协程调度器
  4. 调用协程函数时,同一协程函数的不同部分,有可能在不同的线程环境中执行。因此需要处理好数据争用(Data Race)问题
  5. 需要语言标准和编译器的支持

PS:通过操作系统原子操作或者各种锁机制可以解决数据争用问题


有栈协程和无栈协程对比

  • 有栈协程的最大缺陷是保存调用栈的开销太大
  • 无栈协程不但具有有栈协程的所有优点,而且空间开销极低;唯一不足的是需要语言标准和编译器支持
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C++协程是一种轻量级的线程,可以在单个线程中实现多个协程C++20引入了协程库,其中包括协程原语和一些实用程序,使协程的使用变得更加容易。下面是一个简单的C++协程实现demo: ```c++ #include <iostream> #include <coroutine> struct Generator { struct promise_type { int current_value; Generator get_return_object() { return Generator(std::coroutine_handle<promise_type>::from_promise(*this)); } auto initial_suspend() { return std::suspend_always{}; } auto final_suspend() noexcept { return std::suspend_always{}; } void unhandled_exception() {} void return_void() {} auto yield_value(int value) { current_value = value; return std::suspend_always{}; } }; std::coroutine_handle<promise_type> coroutine; Generator(std::coroutine_handle<promise_type> h) : coroutine(h) {} ~Generator() { if (coroutine) coroutine.destroy(); } int get() { coroutine.resume(); return coroutine.promise().current_value; } }; Generator counter(int begin, int end) { for (int i = begin; i <= end; ++i) co_yield i; } int main() { auto gen = counter(1, 5); while (auto value = gen.get()) std::cout << value << ' '; } ``` 这个demo中,我们定义了一个生成器(Generator)结构体和一个promise_type结构体。Generator结构体包含一个协程句柄,可以通过该句柄来操作协程。promise_type结构体定义了生成器的类型,并提供了返回对象,挂起,终止等方法。在counter函数中,我们使用co_yield关键字来挂起当前协程并返回值。在主函数中,我们使用while循环来不断调用协程的get方法来获取生成的值。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值