20

Kotlin协程实现原理:挂起与恢复

 3 years ago
source link: https://mp.weixin.qq.com/s?__biz=MzIzNTc5NDY4Nw%3D%3D&%3Bmid=2247484696&%3Bidx=1&%3Bsn=069edb2b12c621cde1a36decc4491e5a
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

今天我们来聊聊 Kotlin 的协程 Coroutine

如果你还没有接触过协程,推荐你先阅读这篇入门级文章 What? 你还不知道Kotlin Coroutine?

如果你已经接触过协程,但对协程的原理存在疑惑,那么在阅读本篇文章之前推荐你先阅读下面的文章,这样能让你更全面更顺畅的理解这篇文章。

Kotlin协程实现原理:Suspend&CoroutineContext

Kotlin协程实现原理:CoroutineScope&Job

Kotlin协程实现原理:ContinuationInterceptor&CoroutineDispatcher

如果你已经接触过协程,相信你都有过以下几个疑问:

  1. 协程到底是个什么东西?

  2. 协程的 suspend 有什么作用,工作原理是怎样的?
  3. Job
    Coroutine
    Dispatcher
    CoroutineContext
    CoroutineScope
    
  4. 协程的所谓非阻塞式挂起与恢复又是什么?

  5. 协程的内部实现原理是怎么样的?

  6. ...

接下来的一些文章试着来分析一下这些疑问,也欢迎大家一起加入来讨论。

挂起

协程是使用非阻塞式挂起的方式来保证协程运行的。那么什么是非阻塞式挂起呢?下面我们来聊聊挂起到底是一个怎样的操作。

在之前的文章中提及到 suspend 关键字,它的一个作用是代码调用的时候会为方法添加一个 Continuation 类型的参数,保证协程中 Continuaton 的上下传递。

而它另一个关键作用是起到挂起协程的标识。

协程运行的时候每遇到被 suspend 修饰的方法时,都 有可能 会挂起当前的协程。

注意是有可能。

你可以随便写一个方法,该方法也可以被 suspend 修饰,但这种方法在协程中调用是不会被挂起的。例如

private suspend fun a() {
 println("aa")
}

lifecycleScope.launch {
 a()
}

因为这种方法是不会返回 COROUTINE_SUSPENDED 类型的。

协程被挂起的标志是对应的状态下返回 COROUTINE_SUSPENDED 标识。

更深入一点的话就涉及到 状态机 。协程内部是使用状态机来管理协程的各个挂起点。

文字有点抽象,具体我们还是来看代码。我们就拿上面的 a 方法例子来说明。

首先在 Android Studio 打开这段代码的 Kotlin Bytecode 。可以在 Tools -> Kotlin -> Show Kotlin Bytecode 中打开。

然后点击其中的 Decompile 选项,生成对应的反编译 java 代码。最终代码如下:

BuildersKt.launch$default((CoroutineScope)LifecycleOwnerKt.getLifecycleScope(this), (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
   private CoroutineScope p$;
   Object L$0;
   int label;
 
   @Nullable
   public final Object invokeSuspend(@NotNull Object $result) {
      // 挂起标识
      Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
      CoroutineScope $this$launch;
      switch(this.label) {
      case 0:
         ResultKt.throwOnFailure($result);
         $this$launch = this.p$;
         MainActivity var10000 = MainActivity.this;
         // 保存现场
         this.L$0 = $this$launch;
         // 设置挂起后恢复时,进入的状态
         this.label = 1;
         // 判断是否挂起
         if (var10000.a(this) == var3) {
            // 挂起,跳出该方法
            return var3;
         }
         // 不需要挂起,协程继续执行其他逻辑
         break;
      case 1:
         // 恢复现场
         $this$launch = (CoroutineScope)this.L$0;
         // 是否需要抛出异常
         ResultKt.throwOnFailure($result);
         break;
      default:
         throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
      }

      return Unit.INSTANCE;
   }
 
   @NotNull
   public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
      Intrinsics.checkParameterIsNotNull(completion, "completion");
      Function2 var3 = new <anonymous constructor>(completion);
      var3.p$ = (CoroutineScope)value;
      return var3;
   }
 
   public final Object invoke(Object var1, Object var2) {
      return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
   }
}), 3, (Object)null);

上面的代码就是协程的状态机,通过 label 来代表不同的状态,从而对应执行不同 case 中的逻辑代码。

在之前的文章中已经介绍过,协程启动的时候会手动调用一次 resumeWith 方法,而它对应的内部逻辑就是执行上面的 invokeSuspend 方法。

所以首次运行协程时 label 值为 0 ,进入 case 0: 语句。此时会记录现场为可能被挂起的状态做准备,并设置下一个可能被执行的状态。

如果 a 方法的返回值为 var3 ,这个 var3 对应的就是 COROUTINE_SUSPENDED 。所以只有当 a 方法返回 COROUTINE_SUSPENDED 时才会执行 if 内部语句,跳出方法,此时协程就被挂起。当前线程也就可以执行其它的逻辑,并不会被协程的挂起所阻塞。

所以协程的挂起在代码层面来说就是跳出协程执行的方法体,或者说跳出协程当前状态机下的对应状态,然后等待下一个状态来临时在进行执行。

那为什么说我们写的这个 a 方法不会被挂起呢?

@Nullable
final Object a(@NotNull Continuation $completion) {
   return Unit.INSTANCE;
}

原来是它的返回值并不是 COROUTINE_SUSPENDED

既然它不会被挂起,那么什么情况下的方法才会被挂起呢?

很简单,如果我们在 a 方法中加入 delay 方法,它就会被挂起。

@Nullable
final Object a(@NotNull Continuation $completion) {
   Object var10000 = DelayKt.delay(1000L, $completion);
   return var10000 == IntrinsicsKt.getCOROUTINE_SUSPENDED() ? var10000 : Unit.INSTANCE;
}

真正触发挂起的是 delay 方法,因为 delay 方法会创建自己 Continuation ,同时内部调用 getResult 方法。

 internal fun getResult(): Any? {
     installParentCancellationHandler()
     if (trySuspend()) return COROUTINE_SUSPENDED
     // otherwise, onCompletionInternal was already invoked & invoked tryResume, and the result is in the state
     val state = this.state
     if (state is CompletedExceptionally) throw recoverStackTrace(state.cause, this)
     return getSuccessfulResult(state)
 }

getResult 方法中会通过 trySuspend 来判断挂起当前协程。由挂起自身的协程,从而触发挂起父类的协程。

如果只是为了测试,可以让 a 方法直接返回 COROUTINE_SUSPENDED

 private suspend fun a(): Any {
     return kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED
 }

当然线上千万不能这样写,因为一旦这样写协程将一直被挂起,因为你没有将其恢复的能力。

恢复

现在我们再来聊一聊协程的恢复。

协程的恢复本质是通过 ContinuationresumeWith 方法来触发的。

下面我们来看一个可以挂起的例子,通过它来分析协程挂起与恢复的整个流程。

println("main start")
lifecycleScope.launch {
   println("async start")
   val b = async {
       delay(2000)
       "async"
   }
   b.await()
   println("async end")
}
Handler().postDelayed({
   println("main end")
}, 1000)

Kotlin 代码很简单,当前协程运行与主线程中,内部执行一个 async 方法,通过 await 方法触发协程的挂起。

再来看它的对应反编译 java 代码

// 1
String var2 = "main start";
System.out.println(var2);
BuildersKt.launch$default((CoroutineScope)LifecycleOwnerKt.getLifecycleScope(this), (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
   private CoroutineScope p$;
   Object L$0;
   Object L$1;
   int label;
 
   @Nullable
   public final Object invokeSuspend(@NotNull Object $result) {
      Object var5 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
      CoroutineScope $this$launch;
      Deferred b;
      switch(this.label) {
      case 0:
         // 2
         ResultKt.throwOnFailure($result);
         $this$launch = this.p$;
         String var6 = "async start";
         System.out.println(var6);
         b = BuildersKt.async$default($this$launch, (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
            private CoroutineScope p$;
            Object L$0;
            int label;
 
            @Nullable
            public final Object invokeSuspend(@NotNull Object $result) {
               Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
               CoroutineScope $this$async;
               switch(this.label) {
               case 0:
                  // 3
                  ResultKt.throwOnFailure($result);
                  $this$async = this.p$;
                  this.L$0 = $this$async;
                  this.label = 1;
                  if (DelayKt.delay(2000L, this) == var3) {
                     return var3;
                  }
                  break;
               case 1:
                  // 5、6
                  $this$async = (CoroutineScope)this.L$0;
                  ResultKt.throwOnFailure($result);
                  break;
               default:
                  throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
               }
 
               return "async";
            }
 
            @NotNull
            public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
               Intrinsics.checkParameterIsNotNull(completion, "completion");
               Function2 var3 = new <anonymous constructor>(completion);
               var3.p$ = (CoroutineScope)value;
               return var3;
            }
 
            public final Object invoke(Object var1, Object var2) {
               return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
            }
         }), 3, (Object)null);
         this.L$0 = $this$launch;
         this.L$1 = b;
         this.label = 1;
         if (b.await(this) == var5) {
            return var5;
         }
         break;
      case 1:
         // 7
         b = (Deferred)this.L$1;
         $this$launch = (CoroutineScope)this.L$0;
         ResultKt.throwOnFailure($result);
         break;
      default:
         throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
      }
 
      // 8
      String var4 = "async end";
      System.out.println(var4);
      return Unit.INSTANCE;
   }
 
   @NotNull
   public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
      Intrinsics.checkParameterIsNotNull(completion, "completion");
      Function2 var3 = new <anonymous constructor>(completion);
      var3.p$ = (CoroutineScope)value;
      return var3;
   }
 
   public final Object invoke(Object var1, Object var2) {
      return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
   }
}), 3, (Object)null);
// 4
(new Handler()).postDelayed((Runnable)null.INSTANCE, 1000L);

有点长,没关系我们只看关键点,看它的状态机相关的内容。

  1. 首先会 输出 main start ,然后通过 launch 创建协程,进入协程状态机,此时 label0 ,执行 case: 0 相关逻辑。
  2. 进入 case: 0 输出 async start ,调用 async 并通过 await 来挂起当前协程,再挂起的过程中记录当前挂起点的数据,并将 lable 设置为 1
  3. async
    async
    lable
    0
    async case: 0
    dealy
    async
    label
    1
    2s
    
  4. 此时协程都被挂起,即跳出协程 launch 方法,执行 handler 操作。由于 post 1s 所以比协程中 dealy 还短,所以会优先 输出 main end ,然后再过 1s ,进入恢复协程阶段
  5. async
    delay
    delay
    this
    async
    Continuation
    delay
    2s
    Continuation
    resumeWith
    async
    invokeSuspend
    
  6. async label
    1
    case: 1
    async
    
  7. await
    this
    launch
    Continuation
    resumeWith
    invokeSuspend
    case 1:
    
  8. 最后再继续 输出 async end ,协程运行结束。

我们可以执行上面的代码来验证输出是否正确

main start
async start
main end
async end

我们来总结一下,协程通过 suspend 来标识挂起点,但真正的挂起点还需要通过是否返回 COROUTINE_SUSPENDED 来判断,而代码体现是通过状态机来处理协程的挂起与恢复。在需要挂起的时候,先保留现场与设置下一个状态点,然后再通过退出方法的方式来挂起协程。在挂起的过程中并不会阻塞当前的线程。对应的恢复通过 resumeWith 来进入状态机的下一个状态,同时在进入下一个状态时会恢复之前挂起的现场。

本篇文章主要介绍了协程的挂起与恢复原理,同时也分析了协程的状态机相关的执行过程。希望对学习协程的伙伴们能够有所帮助,敬请期待后续的协程分析。

项目

android_startup:  https://github.com/idisfkj/android-startup

提供一种在应用启动时能够更加简单、高效的方式来初始化组件,优化启动速度。 不仅支持 Jetpack App Startup 的全部功能,还提供额外的同步与异步等待、线程控制与多进程支持等功能。

AwesomeGithub : https://github.com/idisfkj/AwesomeGithub

基于 Github 客户端,纯练习项目,支持组件化开发,支持账户密码与认证登陆。 使用 Kotlin 语言进行开发,项目架构是基于 Jetpack&DataBinding MVVM 项目中使用了 Arouter Retrofit Coroutine Glide Dagger Hilt 等流行开源技术。

flutter_github: https://github.com/idisfkj/flutter_github

基于 Flutter 的跨平台版本 Github 客户端,与 AwesomeGithub 相对应。

android-api-analysis: https://github.com/idisfkj/android-api-analysis

结合详细的 Demo 来全面解析 Android 相关的知识点, 帮助读者能够更快的掌握与理解所阐述的要点。

daily_algorithm https://github.com/idisfkj/daily_algorithm

每日一算法,由浅入深,欢迎加入一起共勉。

推荐阅读

我为何弃用Jetpack的App Startup?

动态代理分析与仿Retrofit实践

重温Retrofit源码,笑看协程实现

感谢老铁们的支持

N7zqQb2.jpg!mobile


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK