什么是协程
- 场景1:异步回调嵌套
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fj2umvsw-1641110474367)(image-20211209143744495.png)]
//客户端顺序进行三次网络异步请求,并用最终结果更新UI
request1(paramter){ value1->
request2(value1){ value2->
request3(value2){ value3->
updateUI(value3)
}
}
}
这种多个回调嵌套耦合非常不利于代码的维护和阅读
-
协程的写法
GlobalScope.launch(Dispatcher.Main){ val value1 = request1() val value2 = request(value1) val value3 = request(value2) updateUI(value3) } suspend request1() suspend request2(..) suspend request3(..)
-
场景2:并发流程控制
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d0W62Zk0-1641110474367)(image-20211209145312666.png)]
fun request1(parameter){ value1 ->
request2(value1) { value2 ->
this.value2 = value2
if(request3){
updateUI()
}
}
request3(value1){ value3 ->
this.value3 = value3
if(request2){
updateUI()
}
}
}
fun updateUI()
这种同时有回调嵌套和并发流控制很容易造成数据的不同步
- 协程的写法
GlobalScope.launch(Dispatchers.Main){
val value1 = request1()
val deferred2 = GlobalScope.async{request2(value1)}
val deferred3 = GlobalScope.async{request3(value1)}
updateUI(deferred2.await(),deferred3.await())
}
suspend request1()
suspend request2(..)
suspend request3(..)
协程的目的是为了让多个任务之间更好的协作,解决异步回调嵌套。能够以同步的方式编排代码完成异步工作。将异步工作像同步代码一样直观,同时它也是一个并发流程控制的解决方案
协程主要是让原来要使用“异步+回调”写出来的复杂代码,简化成看似同步写出来的方式,弱化了线程的概念(对线程操作进一步抽象)
协程的用法
-
引入gradle依赖
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0-RC' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0-RC'
-
常用创建协程的方法
//创建协程时,可以通过Dispatchers.IO、Dispatchers.Main、Dispatchers.Unconfined指定协程运行的线程
val job:Job = GlobalScope.launch(Dispatchers.Main)
val deffered:Deffered = GlobalScope.async(Dispatchers.IO)
Job:协程构建函数的返回值,可以把Job看成协程对象本身,包含了对协程的控制方法
Deffered 是Job的子类,增加了await方法,能够让当前协程暂时挂起,暂停往下执行。当await方法有返回值后再恢复协程,继续往下执行
- 协程的启动
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
}
-
CoroutineContext - 可以理解为协程的上下文,是一种key-value数据结构
-
CoroutineStart - 启动模式,默认是DEFAULT,也就是创建就启动
模式 | 说明 |
---|---|
DEFAULT | 默认模式,创建即启动协程,可随时取消 |
ATOMIC | 自动模式,同样创建即启动,但启动前不可取消 |
LAZY | 延迟启动模式,只有当调用start方法时才会启动 |
协程挂起、恢复原理
-
挂起函数
被关键字
suspend
修饰的方法在编译阶段,编译器会修改方法的返回值、修饰符、入参、方法实现。协程的挂起是靠挂起函数中代码的实现suspend fun request(): String { delay(2 * 1000L) println("after delay") return "result from request1" } /** * 被suspend修饰的request方法经过反编译后稍加修饰后的代码 */ public static final Object request(Continuation completion) {//增加了Continuation参数,返回值变成了Object ContinuationImpl requestContinuation = completion; if((completion.label & Integer.MIN_VALUE) == 0) { requestContinuation = new ContinuationImpl(completion) { @Override Object invokeSuspend(Object o){//会通过DelayKt.delay传入的ContinuationImpl经过resumeWith回调到这。协程被恢复 label |= Integer.MIN_VALUE;//重新给label赋值,以便继续执行协程被挂起之后未运行的代码 return request(this); } }; } switch (requestContinuation.label) { case 0: { requestContinuation.label = 1; Object delay = DelayKt.delay(2000,requestContinuation); if (delay == COROUTINE_SUSPENDED){//协程被挂起 return COROUTINE_SUSPENDED; } } } System.out.println("after delay")//执行协程被挂起之后的代码 return "result from request1" }
-
协程的挂起与恢复
协程的核心是挂起、恢复。挂起、恢复的本质是return & callback 回调
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FLaPw8Pd-1641110474368)(image-20211209174241999.png)]
- 代码原理剖析
示例代码
object CoroutineScene2 { private val TAG: String = "CoroutineScene2" suspend fun request2(): String { delay(2*1000) Log.e(TAG, "request2 completed") return "result from request2" }}
上述代码反编译结果如下:
public final class CoroutineScene2 { private static final String TAG; @NotNull public static final CoroutineScene2 INSTANCE; public final Object request2(@NotNull Continuation var1) { Object $continuation; label20: { if (var1 instanceof <undefinedtype>) {//判断continuation是否被封装过ContinuationImpl,避免协程恢复的时候重复封装 $continuation = (<undefinedtype>)var1; if ((((<undefinedtype>)$continuation).label & Integer.MIN_VALUE) != 0) { ((<undefinedtype>)$continuation).label -= Integer.MIN_VALUE; break label20; } } $continuation = new ContinuationImpl(var1) {//将Continuation包装成ContinuationImpl并添加invokeSuspend // $FF: synthetic field Object result; int label; @Nullable public final Object invokeSuspend(@NotNull Object $result) { this.result = $result; this.label |= Integer.MIN_VALUE; return CoroutineScene2.this.request2(this);//协程恢复执行 } }; } Object $result = ((<undefinedtype>)$continuation).result; Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();//var4赋值成COROUTINE_SUSPENDED switch(((<undefinedtype>)$continuation).label) {//label默认值是0 case 0: ResultKt.throwOnFailure($result); ((<undefinedtype>)$continuation).label = 1;//将label赋值成1 if (DelayKt.delay(2000L, (Continuation)$continuation) == var4) {//DelayKt是异步IO操作会返回COROUTINE_SUSPENDED导致协程挂起 return var4;//request2执行结束,协程挂起 } break; case 1: ResultKt.throwOnFailure($result); break; default: throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine"); } Log.e(TAG, "request2 completed"); return "result from request2"; }}
request2 在添加 suspend 关键字之后编译器会给方法的参数增加一个 Continuation 参数,并且返回值变成了Object,方法实现也做了变化,这个过程被称为:CPS 转换(Continuation-Passing-Style Transformation)。request2 调用进来之后会将传进来的Continuation包装成ContinuationImpl并且添加了invokeSuspend方法,这个方法会在ContinuationImpl调用resumeWith恢复协程的时候被回调。
Continuation封装完之后会调用 DelayKt.delay,因为delay会返回COROUTINE_SUSPENDED将协程挂起,所以request执行到这个地方就return结束了。后面DelayKt在经过2秒的延时之后会通过传进来的ContinuationImpl 调用resumeWith,resumeWith又会调用invokeSuspend,最终将协程恢复。在invokeSuspend中又重新调用request2,将协程被挂起之后的代码继续执行。
总结
-
什么是协程
协程是一种解决嵌套、并发弱化线程概念的解决方案。能让多个任务之间更好的协作,能够以同步的方式编排代码完成异步工作。将异步代码写的像同步代码一样直观
-
协程与线程的区别是什么?
协程基于线程,但相对于线程轻量很多,可理解为在用户层模拟线程操作。
每创建一个协程,都有一个内核态线程动态绑定,用户态下实现调度、切换,真正执行任务的还是内核线程。
线程的上下文切换都需要内核参与,而协程的上下文切换,完全由用户去控制,避免了大量的中断参与,减少了线程上下文切换与调度消耗的资源。
线程是操作系统层面的概念,协程是语言层面的概念线程与协程最大的区别在于:线程是被动挂起恢复,协程是主动挂起恢复
-
协程的启动
根据创建协程指定的调度器HandlerDispacher,DefaultScheduler,UnconfinedDispatcher 来执行任务,以决定协程中的代码块运行在哪个线程上。
-
协程的挂起和恢复
本质是方法的挂起、恢复。本质是return+callback
用编译时的变换处理方法间的callback,这样可以很直观的写顺序执行的异步代码。
-
suspend修饰的方法一定会让协程挂起吗?
只要被
suspend
修饰的函数都是挂起函数,但是不是所有挂起函数都会被挂起。只有当挂起函数里包含异步操作时,它才会被真正挂起。由于suspend
修饰的函数,既可能返回CoroutineSingletons.COROUTINE_SUSPENDED
,表示挂起;也可能返回同步运行的结果,甚至可能返回 null,所以被suspend
修饰的函数的返回值会被编译器改成Object,以适配所有返回值类型