sky’s 雑記

主にAndroidとサーバーサイドの技術について記事を書きます

Coroutine Suspending Functionsを理解したい2

続きの記事です。 Coroutine Suspending Functionsを理解したい1 - sky’s 雑記

振り返り

fun main(args: Array<String>) {
    CoroutineScope(Dispatchers.Main).launch {
      println("start")
      greet()
      println("end")
    }
}

suspend fun greet() {
    println("suspended!!")
}

上記コードをByteコードへCompileするとgreetの引数に Continuation という引数が渡ったコードが生成されることを見ました。

またCompileしたコードをDecompileすると以下のようなコードが生成されました、今回はDecompileしたコードからsuspendの実体を調べていきたいと思います。

...
public static final void main(@NotNull String[] args) {
      Intrinsics.checkParameterIsNotNull(args, "args");
      BuildersKt.launch$default(CoroutineScopeKt.CoroutineScope((CoroutineContext)Dispatchers.getMain()), (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 var5 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
            CoroutineScope $this$launch;
            String var3;
            boolean var4;
            switch(this.label) {
            case 0:
               ResultKt.throwOnFailure($result);
               $this$launch = this.p$;
               var3 = "start";
               var4 = false;
               System.out.println(var3);
               this.L$0 = $this$launch;
               this.label = 1;
               if (TestKt.greet(this) == var5) {
                  return var5;
               }
               break;
            case 1:
               $this$launch = (CoroutineScope)this.L$0;
               ResultKt.throwOnFailure($result);
               break;
            default:
               throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
            }

            var3 = "end";
            var4 = false;
            System.out.println(var3);
            return Unit.INSTANCE;
         }

         @NotNull
         public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
            Intrinsics.checkParameterIsNotNull(completion, "completion");
            Function2 var3 = new Function2(completion) {
               private CoroutineScope p$;
               Object L$0;
               int label;

               @Nullable
               public final Object invokeSuspend(@NotNull Object $result) {

CoroutineScope.launch(Java)

はじめにCoroutineの起動から見ていきます。 Decompileされた以下のコードは

BuildersKt.launch$default(CoroutineScopeKt.CoroutineScope((CoroutineContext)Dispatchers.getMain()), (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {

Kotlinで以下のように実装されています。

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
}

greet()を含むCoroutineScopeに渡されるラムダ式block: suspend CoroutineScope.() -> Unit にあたり new Function2((Continuation)null) に変換されます。Kotlinにおけるラムダ式はFunction2(2とは限らない)というインターフェースを実装したクラスのようで(1)、Coroutineで何が行われるかを理解するには続くFunction2の実装を追いかければ良さそうです。

CoroutineScope.launch(Kotlin)

Javaのコードを追いかけたいところですが、ここで一旦Kotlinのほうのソースコードを眺めてCoroutineの理解を深めたいと思います。 CoroutineScope.launchで渡されたblock(ラムダ式)は以下のように実行されます。CoroutineScopeで実行されるblock全体もsuspendableです。

  • Builders.common.kt
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
}
  • AbstractCoroutine.kt
public fun <R> start(start: CoroutineStart, receiver: R, block: suspend R.() -> T) {
        initParentJob()
        start(block, receiver, this)
    }
  • CoroutineStart.kt
@InternalCoroutinesApi
    public operator fun <T> invoke(block: suspend () -> T, completion: Continuation<T>) =
        when (this) {
            CoroutineStart.DEFAULT -> block.startCoroutineCancellable(completion)
            CoroutineStart.ATOMIC -> block.startCoroutine(completion)
            CoroutineStart.UNDISPATCHED -> block.startCoroutineUndispatched(completion)
            CoroutineStart.LAZY -> Unit // will start lazily
        }

CoroutineScope.launchにおいて LazyStandaloneCoroutine or StandaloneCoroutine を生成します。いずれもAbstractCoroutineを実装しておりAbstractCoroutineは以下のようにContinuationを継承します。

  • AbstractCoroutine.kt
@InternalCoroutinesApi
public abstract class AbstractCoroutine<in T>(
    /**
     * The context of the parent coroutine.
     */
    @JvmField
    protected val parentContext: CoroutineContext,
    active: Boolean = true
) : JobSupport(active), Job, Continuation<T>, CoroutineScope {

CoroutineStartのstartによりblockが実行されるようですが、第2引数としてContinuationを受け取りこれはCoroutineScope.launchにおいて生成したCoroutineそのものです。CoroutineScopeにおいて生成したCoroutine(Continuation)が実行時に伝搬していく様子が徐々に見て取れるようになってきました。

前回の記事で触れた suspend関数は継続渡し方式 (Continuation Passing Style)で実装されている という話が少し実感できるようになったところでここまでにします。

少し脱線しましたが次回はJavaコードに戻りinvokeSuspend/createあたりを深堀りしてsuspend関数がどのように処理を中断するか調べていきます。

参考

(1) 2. 関数型とラムダ式の正体 · Anatomy Kotlin