枯燥的Kotlin协程三部曲(上)——概念篇

0x0、引言


Kotlin 1.3 版本开始引入协程 Coroutine,简练的官方文档 和 网上一堆浅尝辄止的文章让我内心没底,不想止步于仅仅知道:css

① Android中,Kotlin协程用于解决:处理耗时任务保证主线程安全
② 利用Kotlin协程,能够用看起来:同步 的方式编写 异步 代码;
③ 基础的API调用;html

我还想了解更多,如协程的概念,Kotlin协程在实际开发中的使用,背后的原理等,遂有此文。
Kotlin协程的源码还没啃完,此系列目前只能算是笔记,边看边学,部份内容摘取自参考文献,只是让潜意识里有这些概念,后续看完源码,缕清思路再来整理一波,有纰漏之处,欢迎评论区友善指出,一块儿讨论学习,谢谢~
本文主要阐述一些概念相关的东西,一些前置知识,有所了解得可直接跳过。前端


0x一、追根溯源


一、同步 & 异步


先撇开编程相关的东西不说,经过 坐公交 的形象化例子帮助理解 同步与异步:java

乘客排队等公交,车来了,前门扫码上车,一个扫完到下一个扫,一种 串行化 的关系,这是 同步
前门乘客上车,后门乘客下车,互不影响,同时进行,一种 并行化 的关系,这是 异步android

咱们把乘客上车和下车,看作是两个 任务,司机开车也是一个任务,跟这两个任务是异步关系。异步说明二者能够同时进行,乘客还没上完车,司机直接把车开走,也是能够的:git

不过这显然不合常理,正常来讲:司机应该等乘客上下车完毕才发车,那司机怎么知道:github

常规操做有两种:web

轮询(主动):每隔一段时间查看下先后门监控,看下还有没有乘客;
回调(被动):早期的公交车上都会配有一个乘车员,没乘客上下车了,她就会喊司机开车;编程


二、堵塞 & 非堵塞


同步和异步的关注点是 是否同时进行,而堵塞和非堵塞关注的是 可否继续进行,仍是坐公交的例子:安全

有乘客上下车,司机发车就须要等待,此时司机发车的任务处于 堵塞 状态;
乘客都上下车完毕,司机又能够发车了,此时司机发车的任务处于 非堵塞 状态;

堵塞的真正含义:关心的事物因为某些缘由,没法继续进行,所以让你等待。
等待:只是堵塞的一个反作用,代表随时间流逝,没有任何有意义的事物发生或进行。

堵塞时,不必干等着,能够作点其余无关的事物,由于这不影响你对相关事情的等待;
好比司机等发车时,能够喝喝茶、看看手机等,但不能离开。

计算机没人那么灵活,堵塞时干等最容易实现,只需挂起线程,让出CPU便可,等条件知足时,在从新调度此线程。


三、程序


回到编程相关,任务 对应计算机中的 程序,定义以下:

为了完成特定任务,用某种编程语言编写的一组指令集合(一组 静态代码)

CPU处理器逐条执行指令,哪怕出现外部中断,也只是从当前程序切到另外一段程序,继续逐条执行。

和预期一致,代码 逐条执行,但有些业务场景 顺序结构 就无能为力了,好比:

女友:你下班后去超市买10个鸡蛋回来,看到有卖西瓜的就买1个

此时,须要用到四种 基础控制流 中的另一种 → 选择执行

剩下两种基础控制流为 迭代和递归,咱们使用 控制流 来完成 逻辑流,程序执行到哪,逻辑就执行到哪,这样的程序结构清晰,可读性好,比较符合编程人员的思惟习惯,这也是 同步编程 的方式。


四、进程


同一时刻只有一个程序在内存中被CPU调用运行

假设有A、B两个程序,A正在运行,此时须要读取大量输入数据(IO操做),那么CPU只能干等,直到A数据读取完毕,再继续往下执行,A执行完,再去执行程序B,白白浪费CPU资源。

看着有点蠢,能不能这样:

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

固然能够,不过在计算机里 切换 这个名词被细分为两种状态:

挂起:保存程序的当前状态,暂停当前程序;
激活:恢复程序状态,继续执行程序;

这种切换,涉及到了 程序状态的保存和恢复,并且程序A和B所需的系统资源(内存、硬盘等)是不同的,那还须要一个东西来记录程序A和B各自须要什么资源,还有系统控制程序A和B切换,要一个标志来识别等等,因此就有了一个叫 进程的抽象

进程的定义

程序在一个数据集上的一次动态执行过程,通常由下述三个部分组成:

  • 程序:描述进程要完成的功能及如何完成;
  • 数据集:程序在执行过程当中所需的资源;
  • 进程控制块:记录进程的外部特征,描述执行变化过程,系统利用它来控制、管理进程,系统感知进程存在的惟一标志。

进程是系统进行 资源分配和调度 的一个 独立单位

进程的出现使得多个程序得以 并发 执行,提升了系统效率及资源利用率,但存在下述问题:

① 单个进程只能干一件事,进程中的代码依旧是串行执行。
② 执行过程若是堵塞,整个进程就会挂起,即便进程中某些工做不依赖于正在等待的资源,也不会执行。
③ 多个进程间的内存没法共享,进程间通信比较麻烦。

因而划分粒度更小的 线程 出现了。


五、线程

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

线程的定义

轻量级的进程,基本的CPU执行单元,亦是 程序执行过程当中的最小单元,由 线程ID、程序计数器、寄存器组合和堆栈 共同组成。线程的引入减少了程序并发执行时的开销,提升了操做系统的并发性能。

区分:「进程」是「资源分配」的最小单位,「线程」是 「CPU调度」的最小单位

线程和进程的关系

① 一个程序至少有一个进程,一个进程至少有一个线程,能够把进程理解作 线程的容器
② 进程在执行过程当中拥有 独立的内存单元,该进程里的多个线程 共享内存
③ 进程能够拓展到 多机,线程最多适合 多核
④ 每一个独立线程有一个程序运行的入口、顺序执行列和程序出口,但不能独立运行,需依存于应用程序中,由应用程序提供多个线程执行控制;

进程和线程都是一个时间段的描述,是 CPU工做时间段的描述,只是颗粒大小不一样。


六、并发 & 并行


上面提到一个名词 并发 (Concurrency),指的是:

同一时刻只有一条指令执行,但多个进程指令被快速地 轮换执行,使得在宏观上有同时执行的效果,微观上并非同时执行,只是把CPU时间分红若干段,使得多个进程快速交替地执行,存在于单核或多核CPU系统中。

而另外一个容易混淆的名词 并行 (Parallel) 则是:

同一时刻,有多条指令在多个处理器上同时执行,从微观和宏观上看,都是一块儿执行的,存在于多核CPU系统中。


七、协做式 & 抢夺式


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

协做式多任务

早期的操做系统采用的就是协做时多任务,即:

由进程主动让出执行权,如当前进程需等待IO操做,主动让出CPU,由系统调度下一个进程。

每一个进程都循规蹈矩,该让出CPU就让出CPU,是挺和谐的,但也存在一个隐患:

单个进程能够彻底霸占CPU

计算机中的进程参差不齐,先不说那种居心叵测的进程了,若是是健壮性比较差的进程,运行中途发生了死循环、死锁等,会致使整个系统陷入瘫痪!在这种鱼龙混杂的大环境下,把执行权托付给进程自身,确定是不符合基础国情,由操做系统扛大旗的 抢占式多任务 横空出世~

抢占式多任务

由操做系统决定执行权,操做系统具备从任何一个进程取走控制权和使另外一个进程得到控制权的能力。

系统公平合理地为每一个进程分配时间片,进程用完就休眠,甚至时间片没用完,但有更紧急的事件要优先执行,也会强制让进程休眠。有了进程设计的经验,线程也作成了抢占式多任务,但也带来了新的——线程安全问题


八、线程安全问题


进程在执行过程当中拥有独立的内存单元,而多个线程共享这个内存,可能存在这样一种状况:

假设有一个变量a = 10,它能够被线程t1和t2共享访问,两个线程都会对i值进行写入,假设在单核CPU上运行此程序,系统须要给两个线程都分配CPU时间片:

  • 1.t1从内存中读取了a的值为10,它把a的值+1,准备把11这个新值写入内存中,此时时间片耗尽;
  • 2.系统执行了线程调度,t1的执行现场被保存,t2得到执行,它也去读a的值,此时a的值仍为10,+1,而后把11写入内存中;
  • 3.t1再次被调度,此时它也把11写入内存中。

程序的执行结果和咱们的预期不符,a的值应该为12而不是11,这就是线程调度不可预测性引发的 线程同步安全问题

解决方法

系列化访问临界资源,同一时刻,只能有一个线程访问临界资源,也称 同步互斥访问,一般的操做就是 加锁(同步锁),当线程访问临界资源时须要得到这个锁,其余线程没法访问,只能 等待(堵塞),等这个线程使用完释放锁,供其余线程继续访问。

前置概念相关的东西就说这么多,相信会对你接下来学习Kotlin协程大有裨益。


0x二、单线程的Android GUI系统


是的,Android GUI 被设计成单线程了,你可能会问:为啥不采用性能更高的多线程?

答:若是设计成多线程,多个线程同时对一个UI控件进行更新,容易发生 线程同步安全问题;最简单的解决方式:加锁,但这意味着更多的耗时和UI更新效率的下降,并且还有死锁等诸多问题要解决;多线程模型带来的复杂度成本,远远超出它能提供的性能优点成本。这也是大部分GUI系统都是单线程模型的缘由。

Android要求:在主线程(UI线程)更新UI,注意是 → 要求建议,不是规定,规定底线是:

只有建立这个view的线程才能操做这个view

因此,你在子线程中更新子线程建立的UI也是能够的,不过不建议这么作,建议:

在子线程中完成耗时操做,而后经过Handler发送消息,通知UI线程更新UI。

Tips:关于Handler更多的内容可移步至:《换个姿式,带着问题看Handler》

接着说下,Android异步更新UI的写法都有哪些~


一、Handler


主线程中实例化一个Handler对象,在子线程须要更新UI的地方,经过Handler对象的post(runnable)或其余函数,往主线程的消息队列发送消息,等待调度器调度,分发给对应的Handler完成UI更新,写法示例以下:

利用 lambda表达式 + Kotlin语法糖thread { } 可对上述代码进行简化:

还有另一种常见的写法:自定义一个静态内部类Handler,把UI更新操做统一放到这里,根据msg.what进行区分。


二、AsyncTask


AsyncTask是Android提供的一个轻量级的用于处理异步任务的类(封装Handler+Thread),使用代码示例以下:

相比起手写Handler简单了一些,只须要继承AsyncTask,而后就是填空题(按需重写函数):

  • onPreExecute():异步操做开始,能够作一些UI的初始化操做;
  • doInBackground():执行异步操做,可调用publishProgress()触发onProgressUpdate()进度更新;
  • onProgressUpdate():根据进度更新UI;
  • onPostExecute():异步操做完成,更新UI;

但也存在如下局限性:

① AsyncTask类需在主线程中加载;
② AsyncTask对象需在主线程中建立;
③ execute()必须在主线程中调用,且一个AsyncTask对象只能调用一次此方法;
④ 须要为每一种任务类型建立一个特定子类,同时为了访问UI方便,常常定义为Activity的内部类,耦合严重。

能够经过 函数转换为回调的方式 来解耦,抽取后的自定义AsyncTask类以下:

调用也很简单,按需重写对应函数便可:

解耦后灵活多了,外部调用逻辑内部异步逻辑 的分离开来了,但依旧存在问题,如异常处理、任务取消等。


三、runOnUiThread


能够说是很无脑了,在子线程中想更新UI,直接写一个runOnUiThread{}包裹着UI更新相关的代码便可,示例以下:

点进源码康康:

噢,还挺简单:

Activity中定义了此函数,判断当前线程是否为主线程,不是 → Handler.post,是 → 直接执行UI更新。

回调确实是个好东西,可是多层次的回调嵌套,可能会造成 Callback Hell(回调地狱),好比如今有这样的逻辑:

访问百度 → 展现内容(UI) → 下载图标 → 显示图标(UI) → 生成缩略图 → 显示缩略图(UI) → 上传缩略图 → 界面更新(UI)

按照这样的逻辑,用runOnUiThread一把梭,伪代码以下:

老千层饼了,一层套一层,看到这种代码,不知道你是否是和我同样 气抖冷 (据说前端写js回调的更可怕,23333)

常见的规避方法:把嵌套的层次移到外层空间,不使用匿名的回调函数,为每一个回调函数命名。


四、RxJava

RxJava在 链式调用 的设计基础上,经过设置 不一样的调度器,能够灵活地在 不一样线程间切换 并执行对应的Task。
RxJava很强大,但由于较高的学习门槛,大多Android开发仔的认知还停留在:线程切换工具+操做符好用 的阶段。
巧了,笔者也是:

有兴趣深刻学习的RxJava的能够康康《RxJava 沉思录(一):你认为 RxJava 真的好用吗?》
这里只是展现效果,用RxJava写代码的效果,等我变强了,再回来完善这一块:

输出结果以下:

为所欲为,控制线程切换~


五、LiveData


LiveData 是Jetpack提供的一种响应式编程组件,能够包含任何类型的数据,并在数据发生变化时通知给观察者;因为它能够感知并遵循Activity、Fragment或Service等组件的生命周期,所以能够作到仅在组件处于声明周期的激活状态时才更新UI。通常是搭配 ViewModel 组件一块儿使用的。

MutableLiveData是一种可变的LiveData,提供了两种读数据的方法:
主线程中调用的setValue()在非主线程中调用的postValue()

使用时需导入依赖

implementation "androidx.lifecycle:lifecycle-runtime:2.2.0"
复制代码

使用代码示例以下


六、Kotlin协程


使用Kotlin协程须要先添加 协程核心库和平台库 依赖(build.gradle中引入):

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7'
复制代码

使用 withContext 函数能够切换到指定的线程,并在闭包内的逻辑执行结束后,自动把线程切换回上下文继续执行。把RxJava部分的示例改为Kotlin协程的形式,代码示例以下:

使用Kotlin协程后,代码量并无减小,可是异步代码的编写却轻松多了,开始有一种「同步方式写异步代码」的味道了~
再简化下,把withContext 做为函数的返回值。


0x三、Kotlin中的协程究竟是什么


协程

一种 非抢占式(协做式) 的 任务调度模式,程序能够 主动挂起或者恢复执行

与线程的关系

协程基于线程,但相对于线程轻量不少,可理解为在用户层模拟线程操做;每建立一个协程,都有一个内核态进程动态绑定,用户态下实现调度、切换,真正执行任务的仍是内核线程。线程的上下文切换都须要内核参与,而协程的上下文切换,彻底由用户去控制,避免了大量的中断参与,减小了线程上下文切换与调度消耗的资源。

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

  • 有栈协程:有本身的调用栈,可在任意函数调用层级挂起,并转移调度权;
  • 无栈协程:没有本身的调用栈,挂起点的状态经过状态机或闭包等语法来实现;

Kotlin中的协程

"假"协程,Kotlin在语言级别并无实现一种同步机制(锁),仍是依靠Kotlin-JVM的提供的Java关键字(如synchronized),即锁的实现仍是交给线程处理,于是Kotlin协程本质上只是一套基于原生Java Thread API 的封装。

只是这套API 隐藏了异步实现细节,让咱们能够用如同 同步的写法来写异步操做 罢了。


参考文献

相关文章
相关标签/搜索