一、关于协程
协程是Kotlin中最重要、最难学的一块!
为什么协程如此重要?
协程是 Kotlin 对比 Java 的最大优势。Java 也在计划着实现自己的协程:Loom,不过这个毕竟还处于相当初级的阶段。而 Kotlin 的协程,可以帮我们极大地简化异步、并发编程、优化软件架构。通过协程,我们不仅可以提高开发效率,还能提高代码的可读性,由此也就可以降低代码出错的概率。
要记住协程的几个 API 很容易,困难的是形成一套完整的协程知识体系。其实,学习协程,相当于一次编程思维的升级。协程思维,它与我们常见的线程思维迥然不同,当我们能够用协程的思维来分析问题以后,线程当中某些棘手的问题在协程面前都会变成小菜一碟。因此,我们相当于多了一种解决问题的手段。
其实,如果要用简单的语言来描述协程的话,我们可以将其称为:“互相协作的程序”。
///先了解下携程与普通程序执行的区别
fun main() = runBlocking {
val sequence = getSequence()
printSequence(sequence)
}
fun getSequence() = sequence {
println("Add 1")
yield(1)
println("Add 2")
yield(2)
println("Add 3")
yield(3)
println("Add 4")
yield(4)
}
fun printSequence(sequence: Sequence) {
val iterator = sequence.iterator()
val i = iterator.next()
println("Get$i")
val j = iterator.next()
println("Get$j")
val k = iterator.next()
println("Get$k")
val m = iterator.next()
println("Get$m")
}
输出结果如下:
I/System.out: Add 1
I/System.out: Get1
I/System.out: Add 2
I/System.out: Get2
I/System.out: Add 3
I/System.out: Get3
I/System.out: Add 4
I/System.out: Get4
协程与普通程序的区别:
- 普通程序在被调用以后,只会在末尾的地方返回,并且只会返回一次,而协程则不受此限制,协程的代码可以在任意 yield 的地方挂起(Suspend)让出执行权,然后等到合适的时机再恢复(Resume)。在这个情况下,yield 是代表了“让步”的意思。
- 普通程序需要一次性收集完所有的值,然后统一返回;而协程则可以每次只返回(yield)一个值,比如我们前面写的 getSequence() 方法。在这个情况下,yield 既有“让步”的意思,也有“产出”的意思。它不仅能让出执行权,还同时产生一个值,比如前面的 yield(1),就代表产出的值为 1。
除了 yield 以外,我们也可以借助 Kotlin 协程当中的 Channel 来实现类似的代码模式:
// 看不懂代码没关系,目前咱们只需要关心代码的执行结果
fun main() = runBlocking {
val channel = getProducer(this)
testConsumer(channel)
}
fun getProducer(scope: CoroutineScope) = scope.produce {
println("Send:1")
send(1)
println("Send:2")
send(2)
println("Send:3")
send(3)
println("Send:4")
send(4)
}
suspend fun testConsumer(channel: ReceiveChannel) {
delay(100)
val i = channel.receive()
println("Receive$i")
delay(100)
val j = channel.receive()
println("Receive$j")
delay(100)
val k = channel.receive()
println("Receive$k")
delay(100)
val m = channel.receive()
println("Receive$m")
}
执行结果:
I/System.out: Send:1
I/System.out: Receive1
I/System.out: Send:2
I/System.out: Receive2
I/System.out: Send:3
I/System.out: Receive3
I/System.out: Send:4
I/System.out: Receive4
可见,以上代码中的 getProducer() 和 testConsumer() 之间,它们也是交替执行的。
如何理解 Kotlin 的协程?
在 Kotlin 当中,协程是一个独立的框架。跟 Kotlin 的反射库类似,协程并不是直接集成在标准库当中的。如果我们想要使用 Kotlin 的协程,就必须手动进行依赖:
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0'
业界一直有两种说法:
一种是Kotlin 协程其实就是一个封装的线程框架。如果我们站在框架的层面来看的话,这种说法也有一定道理:协程框架将线程池进一步封装,对开发者暴露出统一的协程 API。(扔物线)
另一种是:
从包含关系上看,协程跟线程的关系,有点像线程与进程的关系,毕竟协程不可能脱离线程运行。所以,协程可以理解为运行在线程当中的、更加轻量的 Task。(朱涛)
协程的轻量级
// 直接使用线程
fun main() {
repeat(1000_000_000) {
thread {
Thread.sleep(1000000)
}
}
Thread.sleep(10000L)
}
/*
输出结果:
2022-04-24 17:38:15.736 17290-24330/com.example.conroutinesdemo A/conroutinesdem: thread.cc:4192] Unable to create protected region in stack for implicit overflow check. Reason: Out of memory size: 4096
*/
//使用协程
fun main() =
runBlocking {
repeat(10_000_000){
launch { delay(10000) }
}
delay(10000L)
}
上面这个例子是官方的demo,为了说明协程性能更好,实际上是有漏洞的。上面代码是开了十亿个线程。下面的代码实际上只有一个线程池。所以修改为线程池做对比更好,如下:
fun main() {
val executor = Executors.newSingleThreadExecutor();
val task = java.lang.Runnable {
Thread.sleep(1000)
print(".")
}
repeat(10_000_000){
executor.execute(task)
}
}
还有就是sleep和delay也是有些区别的,实际上我们要做对比应该是使用线程池newSingleThreadExecutor来做对比,这两个实际上是一样的。如下:
fun main(){
val executor = Executors.newSingleThreadScheduledExecutor()
val task = java.lang.Runnable {
print(".")
}
repeat(10_000_000){
executor.schedule(task,1,TimeUnit.SECONDS)
}
}
这样对比之后,性能其实差不多。
如何理解协程的非阻塞?
首选聊一下线程阻塞是什么?简单来说,Android主线程会进行轮询执行任务,如果做一些耗时任务就会导致主线程的阻塞,如何不阻塞,那么就是开线程执行任务。协程实际上也是通过线程来不阻塞主线程,只是写法上看不出来切了线程,看起来就像是非阻塞的。这也是协程的特点,使用同步代码来写异步。其原因就是协程可以自动切走和切回线程,这个过程也叫挂起。
协程的使用
如何启动一个协程
一、使用CoroutineScope.launch()
///通过launch启动协程
fun main(){
//实际开发不用使用GlobalScope 这里只是讲解基础
GlobalScope.launch {
println("协程开始")
delay(1000)
println("hello world")
}
println("按照同步的思维,这应该在协程之后")
//这里休眠两分钟是因为主线程销毁了,协程也不会执行了
Thread.sleep(2000)
println("主程序停止")
}
看下launch的源码
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
....
}
首先是 CoroutineScope.launch(),代表了 launch 其实是一个扩展函数,而它的“扩展接收者类型”是 CoroutineScope。这就意味着,我们的 launch() 会等价于 CoroutineScope 的成员方法。而如果我们要调用 launch() 来启动协程,就必须要先拿到 CoroutineScope 的对象。前面的案例,我们使用的 GlobalScope,其实就是 Kotlin 官方为我们提供的一个 CoroutineScope 对象,方便我们开发者直接启动协程。
接着是第一个参数:CoroutineContext,它代表了我们协程的上下文,它的默认值是 EmptyCoroutineContext,如果我们不传这个参数,默认就会使用 EmptyCoroutineContext。一般来说,我们也可以传入 Kotlin 官方为我们提供的 Dispatchers,来指定协程运行的线程池。
然后是第二个参数:CoroutineStart,它代表了协程的启动模式。如果我们不传这个参数,它会默认使用 CoroutineStart.DEFAULT。CoroutineStart 其实是一个枚举类,一共有:DEFAULT、LAZY、ATOMIC、UNDISPATCHED。我们最常使用的就是 DEFAULT、LAZY,它们分别代表:立即执行、懒加载执行。
第三个参数就是 需要一个无参数,无返回值的挂起函数。
二、runBlocking 启动协程
fun main(){
runBlocking {
println("协程开始")
delay(1000)
println("hello world")
}
println("按照同步的思维,这应该在协程之后")
// Thread.sleep(2000)
println("主程序停止")
}
协程开始
hello world
按照同步的思维,这应该在协程之后
主程序停止
fun main() {
runBlocking {
println("First:${Thread.currentThread().name}")
delay(1000L)
println("Hello First!")
}
runBlocking {
println("Second:${Thread.currentThread().name}")
delay(1000L)
println("Hello Second!")
}
runBlocking {
println("Third:${Thread.currentThread().name}")
delay(1000L)
println("Hello Third!")
}
// 删掉了 Thread.sleep println("Process end!")
}
First:main
Hello First!
Second:main
Hello Second!
Third:main
Hello Third!
从结果就可以看出来,runBlocking是会阻塞主线程的,协程也会阻塞。这种方式一般用来做测试的,代码中尽量少用。
三、async 启动协程
async在dart中也有这么一种方式。它能通过返回的句柄拿到协程执行的结果。
///需要在as的VM options中配置-Dkotlinx.coroutines.debug才能看到
fun main() = runBlocking{
println("In runBlocking : ${Thread.currentThread().name}")
val deferred: Deferred = async {
println("In async: ${Thread.currentThread().name}")
delay(1000)
return@async "任务完成";
}
println("after async : ${Thread.currentThread().name}")
println("${deferred.await()}")
}
In runBlocking : main @coroutine#1
after async : main @coroutine#1
In async: main @coroutine#2
任务完成
async 的协程和runBlocking并不在一个上面,runBlocking本来是阻塞的,但是async的协程在打印语句之后才执行。它是在deferred.await()之后才开始执行的。然后async 可以拿到返回值,这也是async 和launch的区别。
kotlin 挂起函数的核心
- 挂起函数可以极大的简化异步编程,让我们以同步的方式写异步。
- 要定义一个挂起函数,我们只要在普通的函数上面增加一个suspend关键字。
- 挂起函数拥有挂起和恢复的能力,对于同一行代码来说,=左边和右边的代码在不同的线程上,这些都是由- kotlin编译器在做。
- 挂起函数的本质是Callback,只是kotlin底层用了一个高大上的名字叫Contiunation.Kotlin编译器把Suspend 变成Continuation的过程叫做CPS。
- 挂起函数只能在挂起函数中调用,或者是在协程中调用。
协程的生命周期
Job是协程的句柄,当我们用launch或async创建协程的时候,会同时创建一个Job并返回。我们通过job来理解协程的生命周期和并发。
查看launch返回值
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
查看async的返回值
public fun CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyDeferredCoroutine(newContext, block) else
DeferredCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
public interface Deferred : Job {
通过Job我们能干什么?
- 监控协程的生命状态
- 使用Job来操控协程
///协程的生命周期
fun main() = runBlocking{
val job = launch {
delay(1000)
}
job.log()
job.cancel()
job.log()
}
fun Job.log(){
logX("""
isActive = $isActive
isCancelled = $isCancelled
isCompleted = $isCompleted
""".trimIndent())
}
fun logX(any: Any?){
println("""
============================================================
$any
Thread:${Thread.currentThread().name}
============================================================
""".trimIndent())
}
isActive = true 表示协程处于获取阶段
调用job.cancel()以后,协程任务就取消了,isCancel = true 表示协程任务处于取消状态。job.log其实就是对协程的监控,不过是被动的监控。cancel就是对协程的操作。
除了cancel操作,还可以job.start,它一般和CoroutineStart.LAZY一起使用。
协程的监听
///协程的监听
- job.invokeOnCompletion 通过这个api我们可以主动监听协程完成
- job.join 是一个“挂起函数”,它的作用就是:挂起当前的程序执行流程,等待 job 当中的协程任务执行完毕,然后再恢复当前的程序执行流程.它和await的功能是类似的。
fun main() = runBlocking {
suspend fun download() {
//模拟下载任务
val time = (Random.nextDouble() * 1000).toLong()
logX("Delay time = $time")
delay(time)
}
val job = launch(start = CoroutineStart.LAZY) {
logX("Coroutine start!")
download()
logX("Coroutine end!")
}
delay(500)
job.log()
job.start()
job.log()
job.invokeOnCompletion {
//协程执行完成调用这里代码
job.log()
}
//等待协程完毕执行
job.join()
logX("Process end")
}
Job的api
public interface Job : CoroutineContext.Element {
// 省略部分代码
// ------------ 状态查询API ------------
public val isActive: Boolean
public val isCompleted: Boolean
public val isCancelled: Boolean
public fun getCancellationException(): CancellationException
// ------------ 操控状态API ------------
public fun start(): Boolean
public fun cancel(cause: CancellationException? = null)
public fun cancel(): Unit = cancel(null)
public fun cancel(cause: Throwable? = null): Boolean
// ------------ 等待状态API ------------
public suspend fun join()
public val onJoin: SelectClause0
// ------------ 完成状态回调API ------------
public fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle
public fun invokeOnCompletion(
onCancelling: Boolean = false,
invokeImmediately: Boolean = true,
handler: CompletionHandler): DisposableHandle
}
Job与结构化并发
结构化并发是kotlin协程的第二大优势。简单来说就是带有结构和层级的并发。
fun main() = runBlocking{
val parentJob : Job
var job1 : Job? = null
var job2 : Job? = null
var job3 : Job? = null
parentJob = launch {
job1 = launch {
delay(1000)
}
job2 = launch {
delay(3000)
}
job3 = launch {
delay(5000)
}
}
delay(500)
parentJob.children.forEachIndexed{
index, job ->
when (index){
0 -> println("job1 === job is ${job1 === job}")
1 -> println("job2 === job is ${job2 === job}")
2 -> println("job3 === job is ${job3 === job}")
}
}
parentJob.join()
logX("Process end")
}
job1 === job is true
job2 === job is true
job3 === job is true
============================================================
Process end
Thread:main @coroutine#1
============================================================
上面的结果说明嵌套的协程,是属于父子关系, parentJob.join()会等待子job都执行完,才会恢复挂起。
fun main() = runBlocking {
val parentJob: Job
var job1 : Job? = null
var job2 : Job? = null
var job3 : Job? = null
parentJob = launch {
job1 = launch {
logX("Job1 start!")
delay(1000)
logX("Job1 done") //不会走
}
job2 = launch {
logX("Job2 start!")
delay(1000)
logX("Job2 done")//不会走
}
job3 = launch {
logX("Job3 start!")
delay(1000)
logX("Job3 done")//不会走
}
}
delay(500)
parentJob.children.forEachIndexed{
index, job ->
when (index){
0 -> println("job1 === job is ${job1 === job}")
1 -> println("job2 === job is ${job2 === job}")
2 -> println("job3 === job is ${job3 === job}")
}
}
parentJob.cancel();
logX("Process end!")
}
============================================================
Job1 start!
Thread:main @coroutine#3
============================================================
============================================================
Job2 start!
Thread:main @coroutine#4
============================================================
============================================================
Job3 start!
Thread:main @coroutine#5
============================================================
job1 === job is true
job2 === job is true
job3 === job is true
============================================================
Process end!
Thread:main @coroutine#1
============================================================
通过上面的运行结果,可以看出来,实际上parentJob.cancel是会取消掉子job的。
最后,来一个简单的实战优化。
fun main() = runBlocking {
suspend fun getResult1() : String {
delay(1000)//模式耗时操作
return "Result1"
}
suspend fun getResult2() : String {
delay(1000)//模式耗时操作
return "Result2"
}
suspend fun getResult3() : String {
delay(1000)//模式耗时操作
return "Result3"
}
val results = mutableListOf()
val time = measureTimeMillis {
results.add(getResult1())
results.add(getResult2())
results.add(getResult3())
}
println("$time")
println(results)
}
通过结果可以看出来,这个是同步在执行。我们通过协程可以修改成异步的。
fun main() = runBlocking {
suspend fun getResult1(): String {
delay(1000)//模式耗时操作
return "Result1"
}
suspend fun getResult2(): String {
delay(1000)//模式耗时操作
return "Result2"
}
suspend fun getResult3(): String {
delay(1000)//模式耗时操作
return "Result3"
}
val results: List
val time = measureTimeMillis {
var result1 = async { getResult1() }
var result2 = async { getResult2() }
var result3 = async { getResult3() }
results = listOf(result1.await(), result2.await(), result3.await())
}
println("$time")
println("$results")
}
1037
[Result1, Result2, Result3]
CoroutineContext
万物皆context,学习下kotin的context,CoroutineContext。它的最主要的用处是切换线程池。
fun main() = runBlocking {
val user = getUserInfo()
logX(user)
}
suspend fun getUserInfo():String{
logX("Before IO Context")
withContext(Dispatchers.IO){
logX("In IO Context")
delay(1000)
}
logX("After IO Context")
return "BoyCoder"
}
============================================================
Before IO Context
Thread:main @coroutine#1
============================================================
============================================================
In IO Context
Thread:DefaultDispatcher-worker-1 @coroutine#1
============================================================
============================================================
After IO Context
Thread:main @coroutine#1
============================================================
============================================================
BoyCoder
Thread:main @coroutine#1
============================================================
通过上面的结果,我们可以发现withContext可以切换到自定的线程池工作,然后后面的代码会自动切回之前的线程。
讲一下kotlin内置的几个Dispatcher
- **Dispatchers.Main ** 它只在UI编程平台才有意义,在Android、Swing之类的平台上,一般只有Main线程才能绘制UI。
- Dispatchers.Unconfined 代表无所谓,当前协程可以运行在任意线程之上。
- Dispatcher.Default 它代表CPU密集型任务的线程池。一般来说,它内部线程个数跟CPU核心数量保持一致,最小限制是2.
- Dispatcher.IO 它代表IO密集型任务的线程池。它内部的线程数量一般比较多,比如64.
CoroutineScope
在学习launch的时候,我们实际上是有协程作用域的,也就是CoroutineScope。
public interface CoroutineScope {
/**
* The context of this scope.
* Context is encapsulated by the scope and used for implementation of coroutine builders that are extensions on the scope.
* Accessing this property in general code is not recommended for any purposes except accessing the [Job] instance for advanced usages.
*
* By convention, should contain an instance of a [job][Job] to enforce structured concurrency.
*/
public val coroutineContext: CoroutineContext
}
它就一个成员变量CoroutineContext,它是对CoroutineContext的一层封装,主要是用来做批量控制携程。
fun main()= runBlocking {
val scope = CoroutineScope(Job())
scope.launch {
logX("First start")
delay(1000)
logX("First end") //不会执行
}
scope.launch {
logX("Second start")
delay(1000)
logX("Second end")//不会执行
}
scope.launch {
logX("Third start")
delay(1000)
logX("Third end")//不会执行
}
delay(500)
scope.cancel()
delay(1000)
}
============================================================
Second start
Thread:DefaultDispatcher-worker-2 @coroutine#3
============================================================
============================================================
First start
Thread:DefaultDispatcher-worker-1 @coroutine#2
============================================================
============================================================
Third start
Thread:DefaultDispatcher-worker-3 @coroutine#4
============================================================
Process finished with exit code 0
从上面的结果发现: scope.cancel()会直接把三个协程都取消。