第一部分——快速上手
第一章·启程
第二章·基本语法
第三章·Kotlin 与 Java 混编
第二部分——开始学习 Kotlin
第四章·Kotlin 的类特性(上)
第四章·Kotlin 的类特性(下)
第五章·函数与闭包
第六章·集合泛型与操作符
第三部分——Kotlin 工具库
第七章·协程库(上篇)
第七章·协程库(中篇)
如果你觉得我的 Kotlin 博客对你有帮助,那么我强烈建议你看看我的极客时间 Kotlin 视频课程。视频中讲述了很多实际开发中遇到问题的解决办法,以及很多 Kotlin 特性功能在工作中实际项目上的应用场景。
7.协程
协程,协作代码段。相对线程而言,协程更适合于用来实现彼此熟悉的程序组件。协程提供了一种可以避免线程阻塞的能力,这是他的核心功能。在 kotlin 中使用协程,需要在 gradle 中引入协程库:
//Android 工程使用
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:x.x.x"
//Java 工程使用
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:x.x.x'
7.1 协程是什么
协程的概念其实是很早就被提出的。
这里借用知乎作者阿猫的一段回答(有所修改)来为大家讲解协程究竟是怎么来的:
- 一开始大家想要同一时间执行多个代码任务,于是就有了并发。从程序员的角度可以看成是多个独立的逻辑流,内部可以是多 CPU 并行,也可以是单 CPU 时间分片。
- 但是一并发就有上下文切换的问题,干了一半跑去处理另一件事,我这做了一半的东西怎么保存。进程就是这样抽象出来个一个概念,搭配虚拟内存、进程表之类,用来管理独立的程序运行、切换。
- 后来硬件提升了,一电脑上有了好几个 CPU 就可以一人跑一进程,就是所谓的并行。
- 但是一并行,进程数一高,大部分系统资源就得用于进程切换的状态保存。后来搞出线程的概念,大致意思就是这个地方阻塞了,但我还有其他地方的逻辑流可以计算,不用特别麻烦的切换页表、刷新 TLB,只要把寄存器刷新一遍就行。
- 如果你嫌操作系统调度线程有不确定性,不知道什么时候开始什么时候切走,我自己在进程里面手写代码去管理逻辑调度这就是用户态线程。
- 而用户态线程是不可剥夺的,如果一个用户态线程发生了阻塞,就会造成整个进程阻塞,所以进程需要自己拥有调度线程的能力。而如果用户态线程将控制权交给进程,让进程调度自己,这就是协程。
后来我们的内存越来越大,操作系统的调度也越来越智能,就慢慢没人再去花时间去自己实现用户态线程、协程这些东西了。
7.2 为什么又要用协程了
那既然上面说协程已经淘汰在历史的长河中了,为什么现在又跑来这么声势浩大。
这就要从多线程的效率讲起了。
前面我们讲由于操作系统的多线程调度越来越智能,硬件设备也越来越好,这大幅提升了线程的效率,因此正常情况下线程的效率是高于协程的,而且是远高于协程。
那么线程在什么情况下效率是最高的?就是在一直 run 的情况下。但是线程几乎是很难一直 run 的,比如:线程上下文切换、复杂计算阻塞、IO阻塞。
于是又有人想起了协程,这个可以交给代码调度的东西。
7.3 协程的本质作用
协程实际上就是极大程度的复用线程,通过让线程满载运行,达到最大程度的利用CPU,进而提升应用性能。
什么意思呢?
具体一个例子来说:在android上发起一个网络请求。
第1步,主线程创建一个网络请求的任务。
第2步,通过一个子线程去请求服务端响应。
第2.1步,等待网络传递请求,其中可能包括了TCP/IP的一系列过程。
第2.2步,等待服务器处理,比如你请求一个列表数据,服务器逻辑执行依次去缓存、数据库、默认数据找到应该返回给你的数据,再将数据回传给你。
第2.3步,又是一系列的数据回传。
第3步,在子线程中获取到服务器返回的数据。将数据转换成想要的格式。
第4步,在主线程中执行某个回调方法。
在上面这个例子中,第2步通常我们会用一个线程池去存放一批创建好的线程做复用,防止多次创建线程。
但是使用了线程池,就会遇到一个问题。池中预存多少线程才最适合?存少了,后面的任务需要等待有空余的线程才能开始执行;存多了,闲置的线程浪费内存。这个问题实际上还是线程利用率不高的问题。
还是回到示例中,在第2步至2.3步中,此刻线程其实是处于阻塞状态不做事的,这就是线程利用率不高的原因。
这个例子换做协程是这么个流程:
第1步,主线程创建一个协程,在协程中创建网络请求的任务。
第2步,为协程分配一个执行的线程(本例中肯定是子线程),在线程中去请求服务端响应。
第2.1步,(接下来会发生阻塞),挂起子线程中的这个协程,等待网络传递请求,其中可能包括了TCP/IP的一系列过程。
第2.2步,协程依旧处于挂起状态,等待服务器处理,比如你请求一个列表数据,服务器逻辑执行依次去缓存、数据库、默认数据找到应该返回给你的数据,再将数据回传给你。
第2.3步,协程依旧处于挂起状态,又是一系列的数据回传。
第3步,获取到服务器返回的数据,在子线程中恢复挂起的协程。将数据转换成想要的格式。
第4步,在主线程中执行某个回调方法。
在上面这个例子中,整个步骤并没有发生任何改变,但是因为引入了协程挂起的概念。当线程中的协程发生了挂起,线程依旧是可以继续做事的,比如开始执行第二个协程,而协程的挂起是一个很轻的操作(其内在的只是一次状态机的变更,就是一个switch
语句的分支执行,详细内容可以看我的下一篇文章:【Kotlin Primer·第七章·协程库(中篇)】)。这就大大的提升了多任务并发的效率,同时极大的提升了线程的利用率。
这就是协程的本质——极大程度的复用线程,通过让线程满载运行,达到最大程度的利用CPU,进而提升应用性能。
7.4 kotlin 的协程怎么用
在 kotlin 上,使用协程你只需要知道两个方法和他们的返回类型,就可以很熟练的用上协程了。分别是:
fun launch(): Job
fun async(): Deferred
7.4.1 launch方法
从方法名我们就能看出,launch表示启动一个协程。
public fun launch(
context: CoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
}
launch()方法接收三个参数,通常很少用到第二个参数。
第一个参数是一个协程的上下文,CoroutineContext
不仅可以用于在协程跳转的时刻传递数据,同时最主要的功能,是用于表明协程运行与恢复时的上下文环境。
通常Android
在用的时候都是传一个UI
就表示在 UI 线程启动协程,或者传一个CommonPool
表示在异步启动协程,还有一个是Unconfined
表示不指定,在哪个线程调用就在哪个线程恢复。
fun test() {
launch(UI) {
val isUIThread = Thread.currentThread() == Looper.getMainLooper().thread
println("UI::===$isUIThread")
}
launch(CommonPool) {
val isUIThread = Thread.currentThread() == Looper.getMainLooper().thread
println("CommonPool::===$isUIThread")
}
}
例如这段代码就会输出一个
UI::===true
CommonPool::===false
7.4.2 Job对象
launch()
方法会返回一个job
对象,job对象常用的方法有三个,叫start
、join
和cancel
。分别对应了协程的启动、切换至当前协程、取消。
例如下面是start()
方法的使用示例:
fun test() {
//当启动类型设置成LAZY时,协程不会立即启动,而是手动调用start()后他才会启动。
val job = launch(UI, CoroutineStart.LAZY) {
println("hello")
}
job.start()
}
join()
方法就比较特殊,他是一个suspend
方法。suspend
修饰的方法(或闭包)只能调用被suspend
修饰过的方法(或闭包)。 方法声明如下:
public suspend fun join()
因此,join()
方法只能在协程体内部使用,跟他的功能:切换至当前协程所吻合。
fun test() {
val job1 = launch(UI, CoroutineStart.LAZY) {
println("hello1")
}
val job2 = launch(UI) {
println("hello2")
job1.join()
println("hello3")
}
}
这段代码执行后将会输出
hello2
hello1
hello3
7.4.3 async()方法
async()
方法也是创建一个协程并启动,甚至连方法的声明都跟launch()
方法一模一样。
不同的是,async()
方法的返回值,返回的是一个Deferred
对象。这个接口是Job
接口的子类。
因此上文介绍的所有方法,都可以用于Deferred
的对象。
Deferred
最大的用处在于他特有的一个方法await()
:
public suspend fun await(): T
await()
可以返回当前协程的执行结果,也就是你可以这样写代码:
fun test() {
val deferred1 = async(CommonPool) {
"hello1"
}
val deferred2 = async(UI) {
println("hello2")
println(deferred1.await())
}
}
你发现神奇的地方了吗,我让一个工作在主线程的协程,获取到了一个异步协程的返回值。
这意味着,我们以后网络请求、图片加载、数据库、文件操作什么的,都可以丢到一个异步的协程中去,然后在同步代码中直接取返回值,而不再需要去写回调了。
这就是我们经常使用的一个最大特性。
7.5 kotlin 协程使用示例
最后用一个稍微复杂一点的例子,来讲 kotlin 协程的使用
fun test() {
//每秒输出两个数字
val job1 = launch(Unconfined, CoroutineStart.LAZY) {
var count = 0
while (true) {
count++
//delay()表示将这个协程挂起500ms
delay(500)
println("count::$count")
}
}
//job2会立刻启动
val job2 = async(CommonPool) {
job1.start()
"ZhangTao"
}
launch(UI) {
delay(3000)
job1.cancel()
//await()的规则是:如果此刻job2已经执行完则立刻返回结果,否则等待job2执行
println(job2.await())
}
}
最终输出了6次,job1 就被 cancel 了
count::1
count::2
count::3
count::4
count::5
count::6
ZhangTao