Kotlinはコルーチンを内部でどのように実装しますか?
コルーチンはスレッドの「軽量版」であると言われており、コルーチンを実行するために内部的にスレッドを使用していることを理解しています。
ビルダー関数を使用してコルーチンを開始するとどうなりますか?
これは、このコードの実行に関する私の理解です:
_GlobalScope.launch { <---- (A)
val y = loadData() <---- (B) // suspend fun loadData()
println(y) <---- (C)
delay(1000) <---- (D)
println("completed") <---- (E)
}
_
ThreadPool
が事前定義されています。(A)
_で、Kotlinは次に利用可能な空きスレッドでコルーチンの実行を開始します(たとえば_Thread01
_)。(B)
_で、Kotlinは現在のスレッドの実行を停止し、次に使用可能な空きスレッド(_Thread02
_)で中断関数loadData()
を開始します。(B)
_が戻ると、Kotlinはコルーチンを続行します次の使用可能な空きスレッドで(_Thread03
_)。(C)
_は_Thread03
_で実行されます。(D)
_で、_Thread03
_が停止します。(E)
_が次の空きスレッドで実行されます(_Thread01
_など)。私はこれを正しく理解していますか?または、コルーチンは別の方法で実装されていますか?
コルーチンは、ユーザーが記述するスケジューリングポリシーとはまったく別のものです。コルーチンは基本的に_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)
}
_
- Kotlinには、最初に
ThreadPool
が事前定義されています。
スレッドプールがある場合とない場合があります。 UIディスパッチャーはシングルスレッドで動作します。
スレッドがコルーチンディスパッチャーのターゲットになるための前提条件は、スレッドに関連付けられた同時実行キューがあり、スレッドがこのキューからRunnable
オブジェクトを取得して実行するトップレベルのループを実行することです。コルーチンディスパッチャーは、継続をそのキューに置くだけです。
- _
(A)
_で、Kotlinは次に利用可能な空きスレッドでコルーチンの実行を開始します(たとえば_Thread01
_)。
launch
を呼び出したのと同じスレッドにすることもできます。
- _
(B)
_で、Kotlinは現在のスレッドの実行を停止し、次に使用可能な空きスレッド(_Thread02
_)で中断関数loadData()
を開始します。
Kotlinは、コルーチンを一時停止するためにスレッドを停止する必要はありません。実際、コルーチンの主なポイントは、スレッドが開始または停止されないことです。スレッドのトップレベルのループが続き、実行する別の実行可能ファイルを選択します。
さらに、_suspend fun
_を呼び出しているという単なる事実は重要ではありません。コルーチンは、明示的にsuspendCoroutine
を呼び出したときにのみ、一時停止します。関数は、一時停止せずに単純に戻る場合もあります。
しかし、それがsuspendCoroutine
を呼び出したと仮定しましょう。その場合、コルーチンはどのスレッドでも実行されなくなります。これは一時停止されており、どこかのコードがcontinuation.resume()
を呼び出すまで続行できません。そのコードは、将来、いつでも、どのスレッドでも実行される可能性があります。
- 実行後に_
(B)
_が戻ると、Kotlinはコルーチンを続行します次の使用可能な空きスレッドで(_Thread03
_)。
B
は「実行後に戻る」ことはなく、コルーチンはまだ本体の内部にあります。戻る前に何度でも中断および再開できます。
- _
(C)
_は_Thread03
_で実行されます。- _
(D)
_で、_Thread03
_が停止します。- 1000ミリ秒後、_
(E)
_が次の空きスレッドで実行されます(_Thread01
_など)。
ここでも、停止されているスレッドはありません。コルーチンは一時停止され、通常はディスパッチャに固有のメカニズムを使用して、1000ミリ秒後に再開をスケジュールします。その時点で、ディスパッチャに関連付けられている実行キューに追加されます。
具体的には、コルーチンをディスパッチするために必要なコードの例をいくつか見てみましょう。
Swing UIディスパッチャー:
_EventQueue.invokeLater { continuation.resume(value) }
_
Android UIディスパッチャー:
_mainHandler.post { continuation.resume(value) }
_
ExecutorServiceディスパッチャー:
_executor.submit { continuation.resume(value) }
_
コルーチンは、可能な再開ポイントの切り替えを作成することによって機能します。
_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のコルーチンは、いくつかのスケジューラーによって制御される状態マシンにすぎません。