Labs 导读
随着异步编程的发展以及各种并发框架的普及,协程作为一种异步编程规范在各类语言中地位逐步提高。我们不单单会在自己的程序中使用协程,各类框架如fastapi,aiohttp等也都是基于异步以及协程进行实现。
数字化转型时代
用户对计算机
处理效率的要求越来越高
为保证高并发,高性能
在网络请求和程序执行的过程中
大量应用会采用[异步编程]
那作为异步编程范式的一种
协程是怎么做的呢?
它有什么优缺点?
各类语言又是怎么实现协程的?
本期Labs带大家认识下
协程的那些事儿
Part 01、进程,线程到协程
众所周知,计算机操作系统中有两个常见的概念:进程和线程。要讲协程,我们先从这两个基本概念入手。
➢ 进程:操作系统中每一个独立允许的程序,都会占有操作系统分配的资源,是资源分配的基本单位。进程之间互不干涉,都只负责运行自己的指令,这就是进程。
➢ 线程:进程中的一个实体,是被系统独立调度和CPU分派资源的基本单位,线程自己不拥有系统资源,只拥有一些运行时必不可少的资源,如自己的堆栈,程序计数器,寄存器数据等。一个进程可以有多个线程,各个线程共享进程所拥有的全部资源。
➢ 协程:协作的线程,也可以被称作微线程,是一种用户态的线程,协程的调度是由用户主动完成的。代表了一种非抢占式的多任务并发的调度思想:协作式调度,即没有优先级高低的区分。
- 对比
1、从内存占用,上下文切换内容,上下文切换过程等角度进行详细对比。
图片
2、从包容关系上来说,一个进程至少包含一个线程,一个线程里面有0个或者多个协程,因此可归纳为如下图:
Part 02、 从异步编程说起
异步编程,也可以叫做并发编程,并发不同于并行:并行是物理上并行,至少要有2个CPU,两个线程同时运行;而并发可以是单核,通过时间调度算法实现多任务调度,给人感觉是同时运行,实际上某一时刻只有一个线程在运行。异步编程能有效避免主线程被阻塞,特别是对于前端来说,如果主线程被阻塞,会导致APP无响应。常见的异步编程有:多线程,回调,Promise,响应式编程以及协程。
- 多线程
以发微博来举例,发布操作可以简单归结为如下三个操作:
1、获取用户签名数据prepareSubmit
2、携带签名数据进行微博发布内容提交请求postSubmit
3、处理请求,响应结果processPost
最开始我们可能只有10个用户,只需要启动10个线程去操作,但是随着用户数增加到1000个,10000个,这个时候如果启动10000个线程,由于每个线程至少会占用4M,10000个线程会占用39G的内存,对服务器的性能要求太高了,并且线程之间的切换也会占用大量的系统时间。因此这种方式只适用于线程之间没有竞争关系,占用内存资源少,切对时延不敏感的情况。
图片
- 回调
如果用异步回调的等方式解决上面发微博的问题,我们可以用如下代码来解决,这种方式简单易懂,使用范围也很广,几乎所有的异步框架都用到了回调。但是也有很明显的问题:
1、如果步骤很多就会出现嵌套地狱
2、对于异常的情况很难处理和传递
3、如果某一个步骤要等多个回调完成之后再进行收口操作,也很困难
图片
- Promise
Promise是说对于一个耗时比较久的操作,程序给你一个承诺,保证不久之后会把结果告知你。它采用了链式编程模型,简化了回调的异步操作,解决了嵌套地狱的问题,Promise有以下几种状态:
- 待定(pending): 初始状态,既没有被兑现,也没有被拒绝。
- 兑现(fulfilled): 操作成功完成。
- 拒绝(rejected): 操作失败。
发微博问题使用promise来解决如图,必须等前置条件兑现之后才往后。
图片
Promise存在如下问题:
1、每一步的返回值类型都必须是 Promise,不能是实际的数据类型
2、错误处理变得复杂,不同阶段产生的错误很难一路传递下去
3、不同阶段之间共享数据困难
- 响应式编程
响应式编程(Reactive Extension简称Rx)的核心是将一切当作数据流,关注数据流的变换和流转,描述数据输入与输出之间的关系,会实现数量众多的扩展函数,这些函数只对输入和输出负责,因此可以很轻松的将函数分发到其他线程上实现异步调用。但Rx调试比较困难,学习成本较高,维护也不易。
- 协程
考虑到大部分互联网请求都是IO密集型而不是CPU密集型,基本的流程都是:请求-少量计算-调用公共服务-大量读写数据库-返回数据。因此IO密集型很容易发生读写阻塞,此时会进行线程切换,执行其他线程。但线程是宝贵的计算资源,因此我们希望线程不要阻塞,一直跑,不要切换上下文。针对这种需求,协程的优势就出来了。协程执行如图:
图片
★ 优点
1)协程的创建,销毁和调度都发生在用户态,避免CPU频繁切换带来的资源浪费
2)内存占用小,可以轻松创建几十万的协程
3)可读性高,易维护,代码基本等同于同步
4)通过结构化并发限制控制域,减少内存泄漏
Part 03、 种类划分
- 按照调用栈分类
协程最关键的步骤就是暂停代码和恢复代码执行,实现方法主要基于栈和状态机&闭包两种。通过区分执行协程的时候是否可以在任意嵌套函数中被挂起,可以分为有栈协程和无栈协程,有栈协程可以被挂起,无栈协程不能被挂起。先看正常的函数栈操作:
图片
- 有栈协程
协程实现的关键点就是如何保存、恢复和切换上下文,如果将一个函数当作协程,当有栈协程对函数的上下文进行保存,恢复和切换操作时,会对这个函数及其嵌套函数,栈针存储的值,寄存器存储的值进行快照操作,之后只需要对快照做,恢复和切换。
- 无栈协程
相比于有栈协程,无栈协程在不改变调用栈的情况下采用了类似状态机和闭包的方式来存储暂停点的代码信息。在不改变函数调用栈的情况下,我们也不可能在任意一个嵌套函数中挂起协程,这也是无栈协程的特点,同时由于不需要切换栈帧,无栈协程的性能比有栈协程还要高一点。
- 按照调度方式分类
协程的暂停和恢复涉及到控制权的转移,可以分为非对称协程和对称协程。
- 非对称协程
非对称协程通过暂停和继续两个指令进行控制权转移,暂停之后控制权就会转移给继续指令所在的协程,因此控制权的转移存在较弱的调用方和被调用方的关系。
- 对称协程
对称协程只有一个继续指令,各协程之间地位是平等的,继续指令执行之后,控制权就会在多个协程之间流转。
Part 04、 总结
在高并发、高请求当道的今天,合理利用协程势必会提升我们的系统性能和用户体验。当我们的业务操作或者网络请求面临大量IO时,我们可以考虑采用协程替换线程,能够帮助我们的应用降低系统内存占用,同时也减少了系统切换开销,提升系统性能。然而协程虽然很强大,但是也不要过度使用,协程只有和异步IO结合起来才能发挥出最大的威力。