kotlin协程(二)协程的挂起和恢复

179 阅读2分钟

“我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情

上一节我们介绍了kotlin协程的简单用法,以及调度器的含义 kotlin协程详解(一) 这一节我们将看一下协程的挂起与恢复,以及解释上一章中,为什么使用Dispatchers.Unconfined调度器后,代码会运行在kotlin默认的线程池中。

kotlin的协程本质上是方法的return和回调,这部分是编译器帮我们实现的。

被suspend关键字修饰的方法在编译阶段,编译器会修改方法的签名,包括返回值,修饰符,入参,方法体实现。 下图是协程回调的过程。

image.png

object Test {

    private suspend fun request1() : String{
        delay(2 * 1000)
        TLog.dt("TAG","request1")
        return "request1"
    }
}

上面是 kotlin 写的协程代码,经过编译器编译后,会生成如下的 java 代码

public final class Test {
   @NotNull
   public static final Test INSTANCE = new Test();
   public static final int $stable;

   private Test() {
   }

   private final Object request1(Continuation $completion) {
      Object $continuation;
      label20: {
         if ($completion instanceof <undefinedtype>) {
            $continuation = (<undefinedtype>)$completion;
            if ((((<undefinedtype>)$continuation).label & Integer.MIN_VALUE) != 0) {
               ((<undefinedtype>)$continuation).label -= Integer.MIN_VALUE;
               break label20;
            }
         }

         $continuation = new ContinuationImpl($completion) {
            // $FF: synthetic field
            Object result;
            int label;

            @Nullable
            public final Object invokeSuspend(@NotNull Object $result) {
               this.result = $result;
               this.label |= Integer.MIN_VALUE;
               return Test.this.request1((Continuation)this);
            }
         };
      }

      Object $result = ((<undefinedtype>)$continuation).result;
      Object var5 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
      switch (((<undefinedtype>)$continuation).label) {
         case 0:
            ResultKt.throwOnFailure($result);
            ((<undefinedtype>)$continuation).label = 1;
            if (DelayKt.delay(2000L, (Continuation)$continuation) == var5) {
               return var5;
            }
            break;
         case 1:
            ResultKt.throwOnFailure($result);
            break;
         default:
            throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
      }

      TLog var10000 = TLog.INSTANCE;
      Object[] var2 = new Object[]{"request1"};
      TLog.dt$default(var10000, "TAG", var2, false, false, 12, (Object)null);
      return "request1";
   }
}

可以看到,经过 kotlin 编译器后request1 方法被添加了一个Continuation Object 返回值。 该方法的 10 行~31 行是在检测Continuation是否已经被包装成 ContinuationImpl类型的参数。 其中我们重点关注invokeSuspend 方法。他会在被回调的时候在去调用 request1 方法从而恢复方法的运行。

  1. 进入方法后,会进入到 swicth 语句中, 由于continuation是第一次创建, label 为 0。所以会进入 case 0 的分支中。
  2. 这里会进入到DelayKt.delay 方法中
@Nullable
public static final Object delay(long timeMillis, @NotNull Continuation $completion) {
   if (timeMillis <= 0L) {
      return Unit.INSTANCE;
   } else {
      int $i$f$suspendCancellableCoroutine = false;
      Continuation uCont$iv = $completion;
      int var5 = false;
      CancellableContinuationImpl cancellable$iv = new CancellableContinuationImpl(IntrinsicsKt.intercepted(uCont$iv), 1);
      cancellable$iv.initCancellability();
      CancellableContinuation cont = (CancellableContinuation)cancellable$iv;
      int var8 = false;
      if (timeMillis < Long.MAX_VALUE) {
         getDelay(cont.getContext()).scheduleResumeAfterDelay(timeMillis, cont);
      }

      Object var10000 = cancellable$iv.getResult();
      if (var10000 == IntrinsicsKt.getCOROUTINE_SUSPENDED()) {
         DebugProbesKt.probeCoroutineSuspended($completion);
      }

      return var10000 == IntrinsicsKt.getCOROUTINE_SUSPENDED() ? var10000 : Unit.INSTANCE;
   }
}

在这个方法中主要做了 3 件事

  1. 1~11 行将传递进来的Continuation包装成CancellableContinuation类型
  2. 14 行将CancellableContinuation传入scheduleResumeAfterDelay方法中,这个方法就是在子线程中延迟给定的秒数后执行传进来的回调
  3. 接着 17 行会同步调用 getResult 在CancellableContinuationImpl中 getResult 方法会首先调用 trySuspend 方法,验证当前方法的状态,默认是UNDECIDED,导致 if 语句为 true 进入 if 语句中, 从而返回COROUTINE_SUSPENDED。
internal fun getResult(): Any? {
    val isReusable = isReusable()
    // trySuspend may fail either if 'block' has resumed/cancelled a continuation
    // or we got async cancellation from parent.
    if (trySuspend()) {
        /*
         * Invariant: parentHandle is `null` *only* for reusable continuations.
         * We were neither resumed nor cancelled, time to suspend.
         * But first we have to install parent cancellation handle (if we didn't yet),
         * so CC could be properly resumed on parent cancellation.
         *
         * This read has benign data-race with write of 'NonDisposableHandle'
         * in 'detachChildIfNotReusable'.
         */
        if (parentHandle == null) {
            installParentHandle()
        }
        /*
         * Release the continuation after installing the handle (if needed).
         * If we were successful, then do nothing, it's ok to reuse the instance now.
         * Otherwise, dispose the handle by ourselves.
        */
        if (isReusable) {
            releaseClaimedReusableContinuation()
        }
        return COROUTINE_SUSPENDED
    }
    // otherwise, onCompletionInternal was already invoked & invoked tryResume, and the result is in the state
    if (isReusable) {
        // release claimed reusable continuation for the future reuse
        releaseClaimedReusableContinuation()
    }
    val state = this.state
    if (state is CompletedExceptionally) throw recoverStackTrace(state.cause, this)
    // if the parent job was already cancelled, then throw the corresponding cancellation exception
    // otherwise, there is a race if suspendCancellableCoroutine { cont -> ... } does cont.resume(...)
    // before the block returns. This getResult would return a result as opposed to cancellation
    // exception that should have happened if the continuation is dispatched for execution later.
    if (resumeMode.isCancellableMode) {
        val job = context[Job]
        if (job != null && !job.isActive) {
            val cause = job.getCancellationException()
            cancelCompletedResult(state, cause)
            throw recoverStackTrace(cause, this)
        }
    }
    return getSuccessfulResult(state)
}

4. 因为 getResult 返回COROUTINE_SUSPENDED, 导致 Delaykt.delay 方法直接返回。从而使整个方法被 return,导致协成被挂起。这时候我们用 kotlin 编写的代码,最后两句日志代码将不会被打印。 5. 在经过 2s 的延时后,getDelay(cont.getContext()).scheduleResumeAfterDelay(timeMillis, cont); 方法中的回调被调用, 该方法会调用BaseContinuationImpl中的public final void resumeWith(@NotNull Object result)方法

public final void resumeWith(@NotNull Object result) {
   Object current = null;
   current = this;
   Object param = null;
   param = result;

   while(true) {
      DebugProbesKt.probeCoroutineResumed((Continuation)current);
      BaseContinuationImpl $this$resumeWith_u24lambda_u240 = (BaseContinuationImpl)current;
      int var6 = false;
      Continuation var10000 = $this$resumeWith_u24lambda_u240.completion;
      Intrinsics.checkNotNull(var10000);
      Continuation completion = var10000;

      Object outcome;
      Result.Companion var12;
      try {
         outcome = $this$resumeWith_u24lambda_u240.invokeSuspend(param);
         if (outcome == IntrinsicsKt.getCOROUTINE_SUSPENDED()) {
            return;
         }

         var12 = Result.Companion;
         outcome = Result.constructor-impl(outcome);
      } catch (Throwable var11) {
         Throwable exception = var11;
         var12 = Result.Companion;
         outcome = Result.constructor-impl(ResultKt.createFailure(exception));
      }

      Object outcome = outcome;
      $this$resumeWith_u24lambda_u240.releaseIntercepted();
      if (!(completion instanceof BaseContinuationImpl)) {
         completion.resumeWith(outcome);
         return;
      }

      current = completion;
      param = outcome;
   }
}

6. 在上面的方法中,18 行会调用我们的协程方法中的 invokeSuspend方法,在invokeSuspend方法中,我们可以看到,会回调 request1 方法本身,最终kotlin编写的协程代码最后的两行日志将会被打印,这就是协成如何恢复的。 7. 如果返回值还是COROUTINE_SUSPENDED,那么说明上层的方法在挂起状态,直接 return。在来到 34 行。这里调用completion.resumeWith(outcome),resumeWith 方法会逐层调用上层传递的回调,也就是上层方法的本身,最终将结果返回出去。

如果上述流程不够清楚,下一节我将会用 java 来实现协成