前语

随着kotlinAndroid开发范畴越来越火,协程在各个项目中的应用也逐步变得广泛
可是协程终究是什么呢?
协程其实是个陈旧的概念,现已十分成熟了,但大家对它的概念一向存在各种疑问,众说纷乱
有人说协程是轻量级的线程,也有人说kotlin协程其实实质是一套线程切换方案

显然这对初学者不太友好,当不清楚一个东西是什么的时分,就很难进入为什么怎样办的阶段了
本文首要就是答复这个问题,首要包含以下内容
1.关于协程的一些前置知识
2.协程终究是什么?
3.kotlin协程的一些底子概念,挂起函数,CPS转化,情况机等
以上问题总结为思维导图如下:
【带着问题学】协程到底是什么?

1. 关于协程的一些前置知识

为了了解协程,我们可以从以下几个切入点动身
1.什么是进程?为什么要有进程?
2.什么是线程?为什么要有线程?进程和线程有什么差异?
3.什么是协作式,什么是抢占式?
4.为什么要引入协程?是为了处理什么问题?

1.1 什么是进程?

我们在背进程的定义的时分,或许会经常看到一句话

进程是资源分配的最小单位

这个资源分配怎样了解呢?

在单核CPU中,同一时间只需一个程序在内存中被CPU调用工作

假设有AB两个程序,A正在工作,此时需求读取很多输入数据(IO操作),那么CPU只精干等,直到A数据读取完毕,再继续往下实行,A实行完,再去实行程序B,白白浪费CPU资源。

这种方法会浪费CPU资源,我们或许更想要下面这种方法

当程序A读取数据的时,切换 到程序B去实行,当A读取完数据,让程序B暂停,切换 回程序A实行?

在计算机里 切换 这个名词被细分为两种情况:

挂起:保存程序的当时情况,暂停当时程序;
激活:恢复程序情况,继续实行程序;

这种切换,触及到了 程序情况的保存和恢复,并且程序AB所需的系统资源(内存、硬盘等)是不一样的,那还需求一个东西来记录程序AB各自需求什么资源,还有系统控制程序AB切换,要一个标志来辨认等等,所以就有了一个叫 进程的笼统。

1.1.1 进程的定义

进程是一个具有必定独立功用的程序在一个数据集上的一次动态实行的进程,是操作系统进行资源分配和调度的一个独立单位,是应用程序工作的载体
首要由以下三部分组成
1.程序:描绘进程要结束的功用及如何结束;
2.数据集:程序在实行进程中所需的资源;
3.进程控制块:记录进程的外部特征,描绘实行改动进程,系统利用它来控制、处理进程,系统感知进程存在的仅有标志。

1.1.2 为什么要有进程

其实上文我们现已分析过了,操作系统之所以要支撑多进程,是为了前进CPU的利用率
而为了切换进程,需求进程支撑挂起恢复,不同进程间需求的资源不同,所以这也是为什么进程间资源需求隔离,这也是进程是资源分配的最小单位的原因

1.2 什么是线程?

1.2.1 线程的定义

轻量级的进程,底子的CPU实行单元,亦是 程序实行进程中的最小单元,由 线程ID程序计数器寄存器组合仓库 一起组成。
线程的引入减小了程序并发实行时的开销,前进了操作系统的并发功能。

1.2.2 为什么要有线程?

这个问题也很好了解,进程的出现使得多个程序得以 并发 实行,前进了系统功率及资源利用率,但存在下述问题:

  1. 单个进程只精干一件事,进程中的代码依旧是串行实行。
  2. 实行进程假设堵塞,整个进程就会挂起,即便进程中某些作业不依赖于正在等候的资源,也不会实行。
  3. 多个进程间的内存无法同享,进程间通讯比较费事。

线程的出现是为了下降上下文切换消耗,前进系统的并发性,并突破一个进程只精干一件事的缺陷,使得进程内并发成为或许。

1.2.3 进程与线程的差异

  • 1.一个程序至少有一个进程,一个进程至少有一个线程,可以把进程了解做 线程的容器;
  • 2.进程在实行进程中具有 独立的内存单元,该进程里的多个线程 同享内存;
  • 3.进程可以拓展到 多机,线程最多适合 多核;
  • 4.每个独立线程有一个程序工作的入口、次序履队伍和程序出口,但不能独立工作,需依存于应用程序中,由应用程序供应多个线程实行控制;
  • 5.「进程」是「资源分配」的最小单位,「线程」是 「CPU调度」的最小单位
  • 6.进程和线程都是一个时间段的描绘,是 CPU作业时间段的描绘,仅仅颗粒大小不同。

1.3 协作式 & 抢占式

单核CPU,同一时间只需一个进程在实行,这么多进程,CPU的时间片该如何分配呢?

1.3.1 协作式多任务

前期的操作系统选用的就是协作时多任务,即:由进程主动让出实行权,如当时进程需等候IO操作,主动让出CPU,由系统调度下一个进程。
每个进程都安分守己,该让出CPU就让出CPU,是挺谐和的,但也存在一个风险:单个进程可以完全强占CPU
计算机中的进程良莠不齐,先不说那种居心叵测的进程了,假设是健壮性比较差的进程,工作半途产生了死循环、死锁等,会导致整个系统堕入瘫痪!
在这种鱼龙混杂的大环境下,把实行权托付给进程本身,肯定是不科学的,所以由操作系统控制的抢占式多任务横空出世~

1.3.2 抢占式多任务

由操作系统决定实行权,操作系统具有从任何一个进程取走控制权和使另一个进程获得控制权的才能。
系统公平合理地为每个进程分配时间片,进程用完就休眠,甚至时间片没用完,但有更紧急的事件要优先实行,也会强制让进程休眠。
这就是所谓的时间片轮转调度

时间片轮转调度是一种最陈旧,最简略,最公平且运用最广的算法。每个进程被分配一个时间段,称作它的时间片,即该进程允许工作的时间。假设在时间片结束时进程还在工作,则CPU将被掠夺并分配给另一个进程。假设进程在时间片结束前堵塞或结束,则CPU当即进行切换。调度程序所要做的就是维护一张安排稳当进程列表,当进程用完它的时间片后,它被移到行列的结尾。

有了进程规划的经历,线程也做成了抢占式多任务,但也带来了新的——线程安全问题,这个一般通过加锁的方法来处理,这儿就不缀述了

1.4 为什么要引入协程?

上面介绍进程与线程的时分也提到了,之所以引入进程与线程是为了异步并发的实行任务,前进系统功率及资源利用率
但作为Java开发者,我们很清楚线程并发是多么的风险,写出来的异步代码是多么的难以维护。
Java中,我们一般通过回调来处理异步任务,可是当异步任务嵌套时,往往程序就会变得很杂乱与难维护

举个比如,当我们需求结束这样一个需求:查询用户信息 –> 查找该用户的老友列表 –> 查找该老友的动态
看一下Java回调的代码

getUserInfo(new CallBack() {
@Override
public void onSuccess(String user) {
if (user != null) {
System.out.println(user);
getFriendList(user, new CallBack() {
@Override
public void onSuccess(String friendList) {
if (friendList != null) {
System.out.println(friendList);
getFeedList(friendList, new CallBack() {
@Override
public void onSuccess(String feed) {
if (feed != null) {
System.out.println(feed);
}
}
});
}
}
});
}
}
});

这就是传说中的回调阴间,假设用kotlin协程结束相同的需求呢?

val user = getUserInfo()
val friendList = getFriendList(user)
val feedList = getFeedList(friendList)

相比之下,可以说是十分简练了
Kotlin 协程的中心竞争力在于:它能简化异步并发任务,以同步方法写异步代码
这也是为什么要引入协程的原因了:简化异步并发任务

2.终究什么是协程

2.1 什么是协程?

一种非抢占式(协作式)的任务调度形式,程序可以主动挂起或许恢复实行。

2.2 协程与线程的差异是什么?

协程根据线程,但相对于线程轻量很多,可了解为在用户层模仿线程操作;每创立一个协程,都有一个内核态进程动态绑定,用户态下结束调度、切换,实在实行任务的仍是内核线程。
线程的上下文切换都需求内核参加,而协程的上下文切换,完全由用户去控制,避免了很多的中止参加,减少了线程上下文切换与调度消耗的资源。
线程是操作系统层面的概念,协程是言语层面的概念

线程与协程最大的差异在于:线程是被迫挂起恢复,协程是主动挂起恢复

2.3 协程可以怎样分类?

根据 是否拓荒相应的函数调用栈 又分红两类:

  • 有栈协程:有自己的调用栈,可在恣意函数调用层级挂起,并转移调度权;
  • 无栈协程:没有自己的调用栈,挂起点的情况通过情况机或闭包等语法来结束;

2.4 Kotlin中的协程是什么?

“假”协程,Kotlin在言语级别并没有结束一种同步机制(锁),仍是依托Kotlin-JVM的供应的Java要害字(如synchronized),即锁的结束仍是交给线程处理
因而Kotlin协程实质上仅仅一套根据原生Java线程池 的封装。

Kotlin 协程的中心竞争力在于:它能简化异步并发任务,以同步方法写异步代码。
下面介绍一些kotin协程中的底子概念

3. 什么是挂起函数?

我们知道运用suspend要害字修饰的函数叫做挂起函数,挂起函数只能在协程体内或许其他挂起函数内运用.
协程内部挂起函数的调用处被称为挂起点,挂起点假设出现异步调用,那么当时协程就被挂起,直到对应的Continuationresume函数被调用才会恢复实行

我们下面来看看挂起函数详细实行的细节

可以看出kotlin协程可以做到一行代码切换线程
这些是怎样做到的呢,首要是通过suspend要害字

3.1 什么是suspend

suspend 的实质,就是 CallBack

suspend fun getUserInfo(): String {
withContext(Dispatchers.IO) {
delay(1000L)
}
return "BoyCoder"
}

不过当我们写挂起函数的时分,并没有写callback,所谓的callback从何而来呢?
我们看下反编译的效果

//                              Continuation 等价于 CallBack
//                                         ↓         
public static final Object getUserInfo(Continuation $completion) {
...
return "BoyCoder";
}
public interface Continuation<in T> {
public val context: CoroutineContext
//      相当于 onSuccess     效果   
//                 ↓         ↓
public fun resumeWith(result: Result<T>)
}

可以看出
1.编译器会给挂起函数增加一个Continuation参数,这被称为CPS 转化(Continuation-Passing-Style Transformation)
2.suspend函数不能在协程体外调用的原因也可以知道了,就是由于这个Continuation实例的传递

4. 什么是CPS转化

下面用动画演示挂起函数在 CPS 转化进程中,函数签名的改动:
【带着问题学】协程到底是什么?
可以看出首要有两点改动
1.增加了Continuation类型的参数
2.回来类型从String转变成了Any

参数的改动我们之前讲过,为什么回来值要变呢?

4.1 挂起函数回来值

挂起函数通过 CPS 转化后,它的回来值有一个重要作用:标志该挂起函数有没有被挂起。
听起来有点古怪,挂起函数还会不挂起吗?

只需被suspend修饰的函数都是挂起函数,可是不是全部挂起函数都会被挂起
只需当挂起函数里包含异步操作时,它才会被实在挂起

由于 suspend 修饰的函数,既或许回来 CoroutineSingletons.COROUTINE_SUSPENDED,标明挂起
也或许回来同步工作的效果,甚至或许回来 null
为了适配全部的或许性,CPS 转化后的函数回来值类型就只能是 Any?了。

4.2 小结

1.suspend修饰的函数就是挂起函数
2.挂起函数,在实行的时分并不必定都会挂起
3.挂起函数只能在其他挂起函数中被调用
4.挂起函数里包含异步操作的时分,它才会实在被挂起

5. Continuation是什么?

Continuation词源是continue,也就是继续,接下来要做的事的意思
放到程序中Continuation则代表了,接下来要实行的代码
以上面的代码为例,当程序工作 getUserInfo() 的时分,它的 Continuation则是下图红框的代码:
【带着问题学】协程到底是什么?
Continuation 就是接下来要工作的代码,剩下未实行的代码
了解了 Continuation,以后,CPS就简单了解了,它其实就是:将程序接下来要实行的代码进行传递的一种形式
CPS 转化,就是将本来的同步挂起函数转化成CallBack 异步代码的进程。
这个转化是编译器在背后做的,我们程序员对此无感知。
【带着问题学】协程到底是什么?
当然有人会问,这么简略粗暴?三个挂起函数最终变成三个 Callback 吗?
当然不是,思维仍然是CPS的思维,不过需求结合情况机
CPS情况机就是协程结束的中心

6. 情况机

kotlin协程的结束依赖于情况机
想要查看其结束,可以将kotin源码反编译成字节码来查看编译后的代码
关于字节码的分析之前现已有很多人做过了,并且做的很好,可参看:Kotlin Jetpack 实战 | 09. 图解协程原理
读者可通过上面的链接进行详细的学习,下面给出情况机的动画演示

  1. 协程结束的中心就是CPS改换与情况机
  2. 协程实行到挂起函数,一个函数假设被挂起了,它的回来值会是:CoroutineSingletons.COROUTINE_SUSPENDED
  3. 挂起函数实行结束后,通过Continuation.resume方法回调,这儿的Continuation是通过CPS传入的
  4. 传入的Continuation实际上是ContinuationImpl,resume方法最后会再次回到invokeSuspend方法中
  5. invokeSuspend方法就是我们写的代码实行的地方,在协程工作进程中会实行多次
  6. invokeSuspend中通过情况机结束情况的流转
  7. continuation.label 是情况流转的要害,label改动一次代表协程产生了一次挂起恢复
  8. 通过break label结束goTo的跳转作用
  9. 我们写在协程里的代码,被拆分到情况机里各个情况中,分隔实行
  10. 每次协程切换后,都会查看是否产生失常
  11. 切换协程之前,情况机会把之前的效果以成员变量的方法保存在 continuation 中。

以上是情况机流转的大约流程,读者可跟着参看链接,过一下编译后的字节码实行流程后,再来判断这个流程是否正确

7. CoroutineContext是什么?

我们上面说了Continuation是继续要实行的代码,在结束上它也是一个接口

public interface Continuation<in T> {
public val context: CoroutineContext
public fun resumeWith(result: Result<T>)
}

1.Continuation首要由两部分组成,一个context,一个resumeWith方法
2.通过resumeWith方法实行接下去的代码
3.通过context获取上下文资源,保存挂起时的一些情况与资源

CoroutineContext即上下文,首要承载了资源获取,配置处理等作业,是实行环境相关的通用数据资源的共同供应者

CoroutineContext是一个特别的集结,这个集结它既有Map的特征,也有Set的特征
集结的每一个元素都是Element,每个Element都有一个Key与之对应,对于相同KeyElement是不可以重复存在的
Element之间可以通过+号组合起来,Element有几个子类,CoroutineContext也首要由这几个子类组成:

  • Job:协程的仅有标识,用来控制协程的生命周期(newactivecompletingcompletedcancellingcancelled);
  • CoroutineDispatcher:指定协程工作的线程(IODefaultMainUnconfined);
  • CoroutineName: 指定协程的名称,默以为coroutine;
  • CoroutineExceptionHandler: 指定协程的失常处理器,用来处理未捕获的失常.

7.1 CoroutineContext的数据结构

先来看看CoroutineContext的全家福
【带着问题学】协程到底是什么?

public interface CoroutineContext {
//操作符[]重载,可以通过CoroutineContext[Key]这种方法来获取与Key相关的Element
public operator fun <E : Element> get(key: Key<E>): E?
//它是一个调集函数,供应了从left到right遍历CoroutineContext中每一个Element的才能,并对每一个Element做operation操作
public fun <R> fold(initial: R, operation: (R, Element) -> R): R
//操作符+重载,可以CoroutineContext + CoroutineContext这种方法把两个CoroutineContext合并成一个
public operator fun plus(context: CoroutineContext): CoroutineContext
//回来一个新的CoroutineContext,这个CoroutineContext删除了Key对应的Element
public fun minusKey(key: Key<*>): CoroutineContext
//Key定义,空结束,仅仅做一个标识
public interface Key<E : Element>
//Element定义,每个Element都是一个CoroutineContext
public interface Element : CoroutineContext {
//每个Element都有一个Key实例
public val key: Key<*>
//...
}
}

1.CoroutineContext内首要存储的就是Element,可以通过相似map[key] 来取值
2.Element也结束了CoroutineContext接口,这看起来很古怪,为什么元素本身也是集结呢?首要是为了API规划方便,Element内只会寄存自己
3.除了plus方法,CoroutineContext中的其他三个方法都被CombinedContextElementEmptyCoroutineContext重写
4.CombinedContext就是CoroutineContext集结结构的结束,它里面是一个递归定义,Element就是CombinedContext中的元素,而EmptyCoroutineContext就标明一个空的CoroutineContext,它里面是空结束

7.2 为什么CoroutineContext可以通过+号联接

CoroutineContext能通过+号联接,首要是由于重写了plus方法
当通过+号联接时,实际上是包装到了CombinedContext中,并指向上一个Context
【带着问题学】协程到底是什么?
如上所示,是一个单链表结构,在获取时也是通过这种方法去查询对应的key,操作大体逻辑都是先访问当时element,不满足,再访问leftelement,次序都是从rightleft
由于篇幅关系,这儿就不一起看源码了,假设想看源码分析的同学可参看:CoroutineContext的plus操作

总结

本文首要环绕协程终究是什么这全部入点,介绍了关于协程的一些前置知识,协程终究是什么,以及kotlin协程的一些底子概念
关于协程终究是什么,总结如下:
1.一种非抢占式(协作式)的任务调度形式,程序可以主动挂起或许恢复实行
2.Kotlin协程实质上仅仅一套根据原生Java线程池 的封装
3.Kotlin 协程的中心竞争力在于:它能简化异步并发任务,以同步方法写异步代码。
4.kotlin协程在结束上首要依赖于CPS转化与情况机

参看材料

单调的Kotlin协程三部曲(上)——概念启蒙篇
Kotlin Jetpack 实战 | 09. 图解协程原理
揭秘kotlin协程中的CoroutineContext