探秘Kotlin协程机制

什么是协程

  • 场景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,以适配所有返回值类型

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值