web-dev-qa-db-ja.com

Kotlinコルーチンは内部でどのように機能しますか?

Kotlinはコルーチンを内部でどのように実装しますか?

コルーチンはスレッドの「軽量版」であると言われており、コルーチンを実行するために内部的にスレッドを使用していることを理解しています。

ビルダー関数を使用してコルーチンを開始するとどうなりますか?

これは、このコードの実行に関する私の理解です:

_GlobalScope.launch {       <---- (A)
    val y = loadData()     <---- (B)  // suspend fun loadData() 
    println(y)             <---- (C)
    delay(1000)            <---- (D)
    println("completed")   <---- (E)
}
_
  1. Kotlinには、最初にThreadPoolが事前定義されています。
  2. _(A)_で、Kotlinは次に利用可能な空きスレッドでコルーチンの実行を開始します(たとえば_Thread01_)。
  3. _(B)_で、Kotlinは現在のスレッドの実行を停止し、次に使用可能な空きスレッド(_Thread02_)で中断関数loadData()を開始します。
  4. 実行後に_(B)_が戻ると、Kotlinはコルーチンを続行します次の使用可能な空きスレッドで(_Thread03_)。
  5. _(C)_は_Thread03_で実行されます。
  6. _(D)_で、_Thread03_が停止します。
  7. 1000ミリ秒後、_(E)_が次の空きスレッドで実行されます(_Thread01_など)。

私はこれを正しく理解していますか?または、コルーチンは別の方法で実装されていますか?

22
Vishnu Haridas

コルーチンは、ユーザーが記述するスケジューリングポリシーとはまったく別のものです。コルーチンは基本的に_suspend fun_ sの呼び出しチェーンです。一時停止は完全にあなたの管理下にあります:suspendCoroutineを呼び出すだけです。 resumeメソッドを呼び出して中断したところに戻ることができるように、コールバックオブジェクトを取得します。

以下は、サスペンションが完全に制御された非常に直接的で透過的なメカニズムであることを確認できるコードです。

_import kotlin.coroutines.*
import kotlinx.coroutines.*

var continuation: Continuation<String>? = null

fun main(args: Array<String>) {
    val job = GlobalScope.launch(Dispatchers.Unconfined) {
        while (true) {
            println(suspendHere())
        }
    }
    continuation!!.resume("Resumed first time")
    continuation!!.resume("Resumed second time")
}

suspend fun suspendHere() = suspendCancellableCoroutine<String> {
    continuation = it
}
_

コルーチンlaunchは、suspendHere()を呼び出すたびに自分自身を一時停止します。継続コールバックをcontinuationプロパティに書き込み、その継続を明示的に使用してコルーチンを再開します。

コードは、スレッドへのディスパッチをまったく行わないUnconfinedコルーチンディスパッチャーを使用します。コルーチンコードは、continuation.resume()を呼び出す場所で実行されます。


それを念頭に置いて、図をもう一度見てみましょう。

_GlobalScope.launch {       <---- (A)
    val y = loadData()     <---- (B)  // suspend fun loadData() 
    println(y)             <---- (C)
    delay(1000)            <---- (D)
    println("completed")   <---- (E)
}
_
  1. Kotlinには、最初にThreadPoolが事前定義されています。

スレッドプールがある場合とない場合があります。 UIディスパッチャーはシングルスレッドで動作します。

スレッドがコルーチンディスパッチャーのターゲットになるための前提条件は、スレッドに関連付けられた同時実行キューがあり、スレッドがこのキューからRunnableオブジェクトを取得して実行するトップレベルのループを実行することです。コルーチンディスパッチャーは、継続をそのキューに置くだけです。

  1. _(A)_で、Kotlinは次に利用可能な空きスレッドでコルーチンの実行を開始します(たとえば_Thread01_)。

launchを呼び出したのと同じスレッドにすることもできます。

  1. _(B)_で、Kotlinは現在のスレッドの実行を停止し、次に使用可能な空きスレッド(_Thread02_)で中断関数loadData()を開始します。

Kotlinは、コルーチンを一時停止するためにスレッドを停止する必要はありません。実際、コルーチンの主なポイントは、スレッドが開始または停止されないことです。スレッドのトップレベルのループが続き、実行する別の実行可能ファイルを選択します。

さらに、_suspend fun_を呼び出しているという単なる事実は重要ではありません。コルーチンは、明示的にsuspendCoroutineを呼び出したときにのみ、一時停止します。関数は、一時停止せずに単純に戻る場合もあります。

しかし、それがsuspendCoroutineを呼び出したと仮定しましょう。その場合、コルーチンはどのスレッドでも実行されなくなります。これは一時停止されており、どこかのコードがcontinuation.resume()を呼び出すまで続行できません。そのコードは、将来、いつでも、どのスレッドでも実行される可能性があります。

  1. 実行後に_(B)_が戻ると、Kotlinはコルーチンを続行します次の使用可能な空きスレッドで(_Thread03_)。

Bは「実行後に戻る」ことはなく、コルーチンはまだ本体の内部にあります。戻る前に何度でも中断および再開できます。

  1. _(C)_は_Thread03_で実行されます。
  2. _(D)_で、_Thread03_が停止します。
  3. 1000ミリ秒後、_(E)_が次の空きスレッドで実行されます(_Thread01_など)。

ここでも、停止されているスレッドはありません。コルーチンは一時停止され、通常はディスパッチャに固有のメカニズムを使用して、1000ミリ秒後に再開をスケジュールします。その時点で、ディスパッチャに関連付けられている実行キューに追加されます。


具体的には、コルーチンをディスパッチするために必要なコードの例をいくつか見てみましょう。

Swing UIディスパッチャー:

_EventQueue.invokeLater { continuation.resume(value) }
_

Android UIディスパッチャー:

_mainHandler.post { continuation.resume(value) }
_

ExecutorServiceディスパッチャー:

_executor.submit { continuation.resume(value) } 
_
5
Marko Topolnik

コルーチンは、可能な再開ポイントの切り替えを作成することによって機能します。

_class MyClass$Coroutine extends CoroutineImpl {
    public Object doResume(Object o, Throwable t) {
        switch(super.state) {
        default:
                throw new IllegalStateException("call to \"resume\" before \"invoke\" with coroutine");
        case 0:  {
             // code before first suspension
             state = 1; // or something else depending on your branching
             break;
        }
        case 1: {
            ...
        }
        }
        return null;
    }
}
_

このコルーチンを実行する結果のコードは、そのインスタンスを作成し、実行を再開する必要があるたびにdoResume()関数を呼び出します。その処理方法は、実行に使用されるスケジューラーによって異なります。

以下は、単純なコルーチンのコンパイル例です。

_launch {
    println("Before")
    delay(1000)
    println("After")
}
_

このバイトコードにコンパイルするもの

_private kotlinx.coroutines.experimental.CoroutineScope p$;

public final Java.lang.Object doResume(Java.lang.Object, Java.lang.Throwable);
Code:
   0: invokestatic  #18                 // Method kotlin/coroutines/experimental/intrinsics/IntrinsicsKt.getCOROUTINE_SUSPENDED:()Ljava/lang/Object;
   3: astore        5
   5: aload_0
   6: getfield      #22                 // Field kotlin/coroutines/experimental/jvm/internal/CoroutineImpl.label:I
   9: tableswitch   { // 0 to 1
                 0: 32
                 1: 77
           default: 102
      }
  32: aload_2
  33: dup
  34: ifnull        38
  37: athrow
  38: pop
  39: aload_0
  40: getfield      #24                 // Field p$:Lkotlinx/coroutines/experimental/CoroutineScope;
  43: astore_3
  44: ldc           #26                 // String Before
  46: astore        4
  48: getstatic     #32                 // Field Java/lang/System.out:Ljava/io/PrintStream;
  51: aload         4
  53: invokevirtual #38                 // Method Java/io/PrintStream.println:(Ljava/lang/Object;)V
  56: sipush        1000
  59: aload_0
  60: aload_0
  61: iconst_1
  62: putfield      #22                 // Field kotlin/coroutines/experimental/jvm/internal/CoroutineImpl.label:I
  65: invokestatic  #44                 // Method kotlinx/coroutines/experimental/DelayKt.delay:(ILkotlin/coroutines/experimental/Continuation;)Ljava/lang/Object;
  68: dup
  69: aload         5
  71: if_acmpne     85
  74: aload         5
  76: areturn
  77: aload_2
  78: dup
  79: ifnull        83
  82: athrow
  83: pop
  84: aload_1
  85: pop
  86: ldc           #46                 // String After
  88: astore        4
  90: getstatic     #32                 // Field Java/lang/System.out:Ljava/io/PrintStream;
  93: aload         4
  95: invokevirtual #38                 // Method Java/io/PrintStream.println:(Ljava/lang/Object;)V
  98: getstatic     #52                 // Field kotlin/Unit.INSTANCE:Lkotlin/Unit;
 101: areturn
 102: new           #54                 // class Java/lang/IllegalStateException
 105: dup
 106: ldc           #56                 // String call to \'resume\' before \'invoke\' with coroutine
 108: invokespecial #60                 // Method Java/lang/IllegalStateException."<init>":(Ljava/lang/String;)V
 111: athrow
_

これをkotlinc 1.2.41でコンパイルしました

32から76はBeforeを出力し、中断するdelay(1000)を呼び出すためのコードです。

77から101はAfterを印刷するためのコードです。

102から111は、スイッチテーブルのdefaultラベルで示されているように、不正な再開状態のエラー処理です。

つまり、要約すると、kotlinのコルーチンは、いくつかのスケジューラーによって制御される状態マシンにすぎません。

1
Minn