sky’s 雑記

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

Coroutine Suspending Functionsを理解したい1

導入

今更ながらCoroutineのsuspendを不思議に感じることが増えてきて、なんとなくステートマシンで動いているらしいとかいう曖昧な理解のまま使っているのも癪なので本腰入れて調べてみることにする。タイトルに1と書いてあるのはちょっとずつでもアウトプットしたほうが良いだろうという理由と単純に文章量が増えそうだと思ったからである。とりあえず納得できるまでシリーズ化する予定。

Coroutineの説明は特にしないけど、以下のようにCoroutine内でsuspend修飾子がついたfunの処理をどう中断しているのかが気になっている点。

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

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

Continuation

なにはともあれまずはBytecode変換から。以下のprintlnするだけのsuspend funを変換すると

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

こうなる。

public final static greet(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
  @Lorg/jetbrains/annotations/Nullable;() // invisible
    // annotable parameter count: 1 (visible)
    // annotable parameter count: 1 (invisible)
    @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
   L0
    LINENUMBER 4 L0
    LDC "suspended!!"
    ASTORE 1
   L1
    ICONST_0
    ISTORE 2
   L2
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 1
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V
   L3
   L4
    LINENUMBER 5 L4
    GETSTATIC kotlin/Unit.INSTANCE : Lkotlin/Unit;
    ARETURN
   L5
    LOCALVARIABLE $completion Lkotlin/coroutines/Continuation; L0 L5 0
    MAXSTACK = 2
    MAXLOCALS = 3

Bytecodeに変換するとKotlinのコードでは存在しなかったContinuationというものが引数に渡っている。

KotlinのソースコードではContinuationは以下のように定義されている。

メンバーにCoroutineContextを持ちfunにresumeWithを持つインターフェースになっている。コメントをそのまま訳すと中断後の継続を表すインターフェースがContinuationであり、Continuationに対応するCoroutineのコンテキストをメンバーに持ち、resumeWithで対応するCoroutineの実行を再開する、とのこと。

Coroutineにおける処理の実体がContinuationという気配を感じる。

/**
 * Interface representing a continuation after a suspension point that returns a value of type `T`.
 */
@SinceKotlin("1.3")
public interface Continuation<in T> {
    /**
     * The context of the coroutine that corresponds to this continuation.
     */
    public val context: CoroutineContext

    /**
     * Resumes the execution of the corresponding coroutine passing a successful or failed [result] as the
     * return value of the last suspension point.
     */
    public fun resumeWith(result: Result<T>)
}

ここで先程の変換したBytecodeをjavaにDecompileしてみる

public final class TestKt {
   @Nullable
   public static final Object greet(@NotNull Continuation $completion) {
      String var1 = "suspended!!";
      boolean var2 = false;
      System.out.println(var1);
      return Unit.INSTANCE;
   }
}

kotlinのsuspend関数と比較して引数にContinuationが渡っていること以外は大きな差は見られない

次にsuspend関数をCoroutineScopeでラップしたものをDecompileしてみる

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

suspend fun greet() {
    println("suspended!!")
}
...
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) {
                  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;
               }
...

...で省略しているがDecompileした内で public final Continuation create(@Nullable Object value, @NotNull Continuation completion) { が無限にネストしそうな気配だった。(Decompile処理自体がいつまでたっても終了しなかった)

上記のコードを見てsuspend関数がcontinuation passing-styleで実装されているという話とstate machineが少しだけ紐付いてきた気がする。

続きはまた今度。

参考

1) Suspending functions, coroutines and state machines - Pedro Felix Labs

2) https://www.infoq.com/jp/articles/kotlin-coroutines-bottom-up/

3) https://medium.com/google-developer-experts/coroutines-suspending-state-machines-36b189f8aa60

4) https://github.com/Kotlin/KEEP/blob/master/proposals/coroutines.md