现在的位置: 首页 > 自动控制 > 工业·编程 > 正文

C++协程综述

2017-05-17 08:16 工业·编程 ⁄ 共 2404字 ⁄ 字号 暂无评论

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

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

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

    多线程

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

    1)创建线程非常耗时

    2)线程上下文切换非常耗时(要从用户态切换到内核态;保存当前线程执行环境;加载目标线程的执行环境;再从内核态切换回来等一系列操作)

    3)每个线程会预先分配一个几M的调用栈

    4)系统中最多只能同时运行数千个线程(当就绪线程个数远大于CPU核数,操作系统大量的时间就会用来进行线程切换,性能急剧下降)

    异步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可以全速并行执行,使用率大大提高

    协程

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

    1)协程的特点在于是一个线程执行,最大的优势就是协程极高的执行效率。因为创建或切换协程不是线程创建/切换,它相当于一次函数调用,由程序自身控制,和多线程比,线程数量越多,协程的性能优势就越明显。

    2)协程不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。

    3)每个协程通常只需要几十个字节保存相关状态信息,空间开销远低于线程栈;

    4)系统中可同时运行数千万个协程,数量主要取决于可用内存大小(当然cpu核数也限制)

    5)协程在性能与异步IO同样,但是在逻辑上和同步IO一样直观

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

    处理IO密集型任务

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

    1)侦听线程收到一个客户端请求

    2)侦听线程吧请求加入一个队列,然后继续侦听下一个请求

    3)工作线程从队列种取出一个请求,然后发起IO

    4)IO完成后,向客户端返回请求结果

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

    20190914102449708

    三种架构的对比

    20190914102629394

    协程的实现机制

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

    普通函数:每次调用只能从第一条语句开始执行

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

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

    协程函数特点

    1)首次调用协程函数,会从堆中分配一个协程上下文,调用方的返回地址、入口函数、交出控制权等信息保存在协程上下文中

    2)当协程中途交出控制权后,协程上下文不会被删除(相当于函数退出之后,上下文环境还被保存了,类比线程切换)

    3)当协程再次获得控制权后,会自动从协程上下文中恢复调用环境,然后从上一次交出控制权的下一条语句继续执行(加载目标协程环境,类比线程切换)

    4)协程函数返回(非中途交出控制权)后,协程上下文将被删除

    5)若再次调用协程函数,视为首次调用

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

    有栈协程

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

    20190614165143797

    有栈协程的特点

    ps1:为了减少有栈协程的空间开销,有些协程框架用一个共享栈代替每个协程的私有栈。共享栈虽然降低了协程的空间开销,但却引进了栈拷贝的时间开销。

    ps2:对称协程调度逻辑复杂,应用的场景有限,非对称协程是有栈协程的主流

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

    20190914103522165

    无栈协程

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

    无栈协程特点

    2019061417314566

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

    有栈协程和无栈协程对比

    1)有栈协程的最大缺陷是保存调用栈的开销太大

    2)无栈协程不但具有有栈协程的所有优点,而且空间开销极低;唯一不足就是需要语言标准和编译器支持

    给我留言

    留言无头像?