《Kotlin协程》均基于Kotlinx-coroutines 1.3.70
《Kotlin协程》阅读顺序
· Kotlin协程-协程派发和调度框架
· Kotlin协程-一个协程的生命周期
· Kotlin协程-特殊的阻塞协程
· Kotlin协程-协程的内部概念Continuation
· Kotlin协程-work steal的实现
· Kotlin协程-Scheduler的优秀设计
· 协程是什么?
· 什么时候用协程?
· 协程的核心是什么?
· kotlin的协程和其他语言的协程有什么异同?
kotlin的协程的出现其实比kotlin语言还晚一点。在当前这个版本,协程甚至都还处于一个不稳定的迭代版本中。协程到目前为止都还没进入kotlin的标准库,它是一个独立的依赖库,叫 Kotlinx。对于想在开发中使用协程的人来说,需要在依赖里加入kotlinx-core依赖。作为一个独立的依赖包,它的源码可以从github上获取,《Kotlin协程》分析的源码就是以github上的master分支为参考。
协程的出现是为了解决异步编程中遇到的各种问题。从高级编程语言出现的第一天,异步执行的问题就伴随出现。
在Kotlin里使用协程非常方便,
import kotlinx.coroutines.*
fun main() {
GlobalScope.launch {
// 在后台启动一个新的协程并继续
delay(1000L) // 非阻塞的等待 1 秒钟(默认时间单位是毫秒)
println("World!") // 在延迟后打印输出
}
println("Hello,") // 协程已在等待时主线程还在继续
Thread.sleep(2000L) // 阻塞主线程 2 秒钟来保证 JVM 存活
}
上面的代码是一个常规启动协程的方式,关键函数只有 launch,delay,这两个函数是kotlin协程独有的。其他函数都属于基本库。
代码的输出结果是
Hello,
World!
这是一个典型的异步执行结果,先得到 Hello,而不是按代码顺序先得到 World。
异步执行在平时开发中经常遇到,比如执行一段IO操作,不管是文件读写,还是网络请求,都属于IO。
在Android中我们对IO操作的一个熟知的规则是不能写在主线程中,因为它会卡线程,导致ANR。而上面的代码其实是不会卡线程的。
用同步的方式写异步代码
这句话在很多资料中出现过,划重点。
理解这句话的关键在于,协程干了什么,让这个异步操作不会卡主线程?
我们知道类似的技术在RxJava中也有,它通过手动切线程的方式指定代码运行所在的线程,从而达到不卡主线程的目的。而协程的高明和简洁之处在于,开发者不需要主动切线程。
在上面的代码中打印一下线程名观察结果。
import kotlinx.coroutines.*
fun main() {
GlobalScope.launch {
// 在后台启动一个新的协程并继续
println("Thread: ${Thread.currentThread().name}")
delay(1000L) // 非阻塞的等待 1 秒钟(默认时间单位是毫秒)
println("World!") // 在延迟后打印输出
}
println("Thread: ${Thread.currentThread().name}")
println("Hello,") // 协程已在等待时主线程还在继续
Thread.sleep(2000L) // 阻塞主线程 2 秒钟来保证 JVM 存活
}
我们会得到
Thread: main
Hello,
Thread: DefaultDispatcher-worker-1
World!
可以看到在打印World的时候,代码是运行在子线程的。
对于经常用协程开发的人来说,有几个很有意思的问题值得思考下。
· 上面代码中的Thread.sleep()可以改成delay()吗?
· 为什么理论上可以开无限多个coroutine?
· 假设有一个IO操作 foo() 耗时a,一个计算密集操作 bar() 耗时b,用协程来执行的话,launc{a b} 耗时c,c是否等于a + b?
另外一个很有意思的问题需要用代码来展示。在协程中有一个函数 runBlocking{},没接触过的可以简单理解为它等价于launch{}。
用它来改造上面的代码,
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
GlobalScope.launch {
// 在后台启动一个新的协程并继续
println("Thread: ${Thread.currentThread().name}")
delay(1000L) // 非阻塞的等待 1 秒钟(默认时间单位是毫秒)
println("World!") // 在延迟后打印输出
}
println("Thread: ${Thread.currentThread().name}")
println("Hello,") // 协程已在等待时主线程还在继续
Thread.sleep(2000L) // 阻塞主线程 2 秒钟来保证 JVM 存活
}
我们会得到
Thread: DefaultDispatcher-worker-1
Thread: main
Hello,
World!
现在我们把 GlobalScope.launch这行改造一下,
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
launch {
// 在后台启动一个新的协程并继续
println("Thread: ${Thread.currentThread().name}")
delay(1000L) // 非阻塞的等待 1 秒钟(默认时间单位是毫秒)
println("World!") // 在延迟后打印输出
}
println("Thread: ${Thread.currentThread().name}")
println("Hello,") // 协程已在等待时主线程还在继续
Thread.sleep(2000L) // 阻塞主线程 2 秒钟来保证 JVM 存活
}
现在再看执行结果,
Thread: main
Hello,
Thread: main
World!
WTF? launch里的代码也执行在主线程了?
这个问题涉及到Kotlin协程的Scope,调度,也是协程的实现核心逻辑
实际上在Kotlin之前就有不少语言实践了协程这个概念。比如python,golang。
而最原始的协程其实不叫协程,叫纤程(Fiber)。听说过Fiber的人都已经。。
甲:听说过纤程吗
乙:Fiber是吧
甲:你今年起码40岁了吧
纤程是微软第一个提出的,但因为它的使用非常的反人类,对程序员的代码质量要求非常高,以至于没人愿意用它。虽然现在还可以在微软官网上找到关于纤程的资料,但能用好纤程的程序员凤毛麟角。
Using Fibers
直到golang的出现,才把协程这个技术发扬光大。有人说python也有协程呀,为什么是golang。
其实python的协程不是真正意义上的协程,后面我们会说到。python的协程是基于yield关键字进行二次封装的,虽然在高层抽象上也是以函数作为协程粒度,但对比golang差的太远。
golang的协程叫goroutine,跟kotlin的coroutine差不多。golang用一种程序员更容易理解的抽象定义了协程粒度goroutine,还有它的各种操作。
对于程序员来说,再也不用关心什么时候切协程,协程在什么线程运行这种问题,开发效率和代码运行效率得到成倍提升。
golang在编译器上做了很多优化,当代码中发生IO或者内核中断的时候,会自动帮你切协程。熟悉计算机原理的能明白,当发生内核中断的时候,比如请求一个磁盘文件,中断发生时CPU其实是没有工作的,执行逻辑在这个时候处于一个空转,直到中断返回结果才继续往下执行。
于是在中断发生的时候,CPU相当于浪费了一段时间。golang在这个时候切协程,就能把CPU浪费的算力利用起来交给另外一个协程去执行。
如果去看kotlin的协程源码的话会发现里面有很多 exeprimental 的api和实现逻辑。直到1.3.70为止,jetbain团队还在继续地为coroutine机制增加新的活力。目前来说coroutine处于一个稳定阶段,可以基于1.3.70版本来分析它,后面应该不会有很大机制上的变动了。