Kotlin コルーチンの実装原理
Kotlin
今日はコルーチンについてお話しますCoroutine
。
コルーチンに触れたことがあれば、次のような疑問があると思います。
- コルーチンとは正確には何ですか?
- コルーチンの役割
suspend
と動作原理は何ですか? - コルーチン内のいくつかのキー名 ( 、
Job
、Coroutine
、Dispatcher
などCoroutineContext
)CoroutineScope
の関係は何ですか? - コルーチンのいわゆる非ブロッキング中断および回復とは何ですか?
- コルーチンの内部実装原則は何ですか?
- …
次のいくつかの記事では、これらの質問を分析しようとします。誰もが議論に参加することを歓迎します。
電話を切る
コルーチンはノンブロッキング サスペンションを使用して、コルーチンが確実に実行されるようにします。ノンブロッキングサスペンドとは?電話を切るとはどのような操作なのかお話しましょう。
キーワードについては前回の記事で触れましたがsuspend
、その機能の 1 つは、コードが呼び出されると、Continuation
型パラメーターがメソッドに追加され、コルーチンでの上下の転送が確実に行われるようにすることですContinuaton
。
もう 1 つの重要な役割は、中断されたコルーチンを識別することです。
コルーチンの実行中に変更されたメソッドが検出されると、現在のコルーチンが中断される場合があります。suspend
注意が可能です。
メソッドは自由に記述でき、メソッドをsuspend
変更することもできますが、このメソッドはコルーチンで呼び出されたときに中断されません。例えば
private suspend fun a() {
println("aa")
}
lifecycleScope.launch {
a()
}
COROUTINE_SUSPENDED
このメソッドは型を返さないためです。
コルーチンが中断されているというフラグは、対応する state の return フラグですCOROUTINE_SUSPENDED
。
もう少し深く掘り下げるには、ステート マシンが関係します。コルーチンの内部では、コルーチンの各中断ポイントを管理するためにステート マシンが使用されます。
テキストは少し抽象的です。コードを具体的に見てみましょう。上記のa
方法を例として説明します。
最初にAndroid Studio
このコードを開きますKotlin Bytecode
。で開くことができますTools -> Kotlin -> Show Kotlin Bytecode
。
次に、Decompile
オプションの 1 つをクリックして、対応する逆コンパイルされたjava
コードを生成します。最終的なコードは次のとおりです。
BuildersKt.launch$default((CoroutineScope)LifecycleOwnerKt.getLifecycleScope(this), (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 var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
CoroutineScope $this$launch;
switch(this.label) {
case 0:
ResultKt.throwOnFailure($result);
$this$launch = this.p$;
MainActivity var10000 = MainActivity.this;
// 保存现场
this.L$0 = $this$launch;
// 设置挂起后恢复时,进入的状态
this.label = 1;
// 判断是否挂起
if (var10000.a(this) == var3) {
// 挂起,跳出该方法
return var3;
}
// 不需要挂起,协程继续执行其他逻辑
break;
case 1:
// 恢复现场
$this$launch = (CoroutineScope)this.L$0;
// 是否需要抛出异常
ResultKt.throwOnFailure($result);
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
return Unit.INSTANCE;
}
@NotNull
public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
Intrinsics.checkParameterIsNotNull(completion, "completion");
Function2 var3 = new <anonymous constructor>(completion);
var3.p$ = (CoroutineScope)value;
return var3;
}
public final Object invoke(Object var1, Object var2) {
return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
}
}), 3, (Object)null);
上記のコードはコルーチンのステートマシンであり、label
異なる状態を表すために使用され、case
対応する異なるロジックコードを実行します。
前回の記事で述べたように、コルーチンが開始されると、resumeWith
メソッドが手動で 1 回呼び出され、それに対応する内部ロジックが上記のinvokeSuspend
メソッドを実行します。
したがって、コルーチンが初めて実行されるとき、label
値は であり0
、case 0:
ステートメントに入ります。このとき、記録サイトは、一時停止される可能性のある状態に備え、次に実行される可能性のある状態を設定します。
a
メソッドが値を返す場合var3
、これはvar3
それに対応しますCOROUTINE_SUSPENDED
。a
したがって、内部ステートメントはメソッドが返されたときにのみCOROUTINE_SUSPENDED
実行されif
、メソッドは飛び出し、この時点でコルーチンは中断されます。現在のスレッドは他のロジックも実行でき、コルーチンの中断によってブロックされることはありません。
したがって、コードレベルでは、コルーチンの一時停止は、コルーチン実行のメソッド本体から飛び出すか、コルーチンの現在のステートマシンの下で対応する状態から飛び出し、次の状態が来るのを待つことです。そしてそれを実行します。
では、なぜ私たちが書いたメソッドがa
中断されないと言うのでしょうか?
@Nullable
final Object a(@NotNull Continuation $completion) {
return Unit.INSTANCE;
}
その戻り値は ではないことがわかりましたCOROUTINE_SUSPENDED
。
停止しないということは、どのような場合に停止するのでしょうか?
メソッドにメソッドをa
入れると、中断されます。delay
@Nullable
final Object a(@NotNull Continuation $completion) {
Object var10000 = DelayKt.delay(1000L, $completion);
return var10000 == IntrinsicsKt.getCOROUTINE_SUSPENDED() ? var10000 : Unit.INSTANCE;
}
メソッドは内部的にメソッドを呼び出している間にそれ自体を作成するdelay
ため、実際に hang をトリガーするのはメソッドです。delay
Continuation
getResult
internal fun getResult(): Any? {
installParentCancellationHandler()
if (trySuspend()) return COROUTINE_SUSPENDED
// otherwise, onCompletionInternal was already invoked & invoked tryResume, and the result is in the state
val state = this.state
if (state is CompletedExceptionally) throw recoverStackTrace(state.cause, this)
return getSuccessfulResult(state)
}
getResult
メソッドでは、trySuspend
現在のコルーチンを中断すると判断されます。親クラスのコルーチンの一時停止は、それ自体のコルーチンを一時停止することによってトリガーされます。
テストだけの場合は、a
メソッドを直接返すことができますCOROUTINE_SUSPENDED
private suspend fun a(): Any {
return kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED
}
もちろん、このようにオンラインで書いてはいけません。コルーチンをこのように書いたら、再開することができないため、常に中断されます。
回復
それでは、コルーチンの回復について話しましょう。
コルーチンのリカバリー エッセンスは、メソッドによってトリガーされますContinuation
。resumeWith
一時停止できる例を見て、それを使用して、コルーチンの一時停止と回復のプロセス全体を分析してみましょう。
println("main start")
lifecycleScope.launch {
println("async start")
val b = async {
delay(2000)
"async"
}
b.await()
println("async end")
}
Handler().postDelayed({
println("main end")
}, 1000)
Kotlin
コードは非常に単純で、現在のコルーチンはメイン スレッドで実行され、コルーチンの一時停止をトリガーするasync
メソッドが内部で実行されます。await
java
対応する逆コンパイルされたコードを見てみましょう
// 1
String var2 = "main start";
System.out.println(var2);
BuildersKt.launch$default((CoroutineScope)LifecycleOwnerKt.getLifecycleScope(this), (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
private CoroutineScope p$;
Object L$0;
Object L$1;
int label;
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
Object var5 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
CoroutineScope $this$launch;
Deferred b;
switch(this.label) {
case 0:
// 2
ResultKt.throwOnFailure($result);
$this$launch = this.p$;
String var6 = "async start";
System.out.println(var6);
b = BuildersKt.async$default($this$launch, (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 var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
CoroutineScope $this$async;
switch(this.label) {
case 0:
// 3
ResultKt.throwOnFailure($result);
$this$async = this.p$;
this.L$0 = $this$async;
this.label = 1;
if (DelayKt.delay(2000L, this) == var3) {
return var3;
}
break;
case 1:
// 5、6
$this$async = (CoroutineScope)this.L$0;
ResultKt.throwOnFailure($result);
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
return "async";
}
@NotNull
public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
Intrinsics.checkParameterIsNotNull(completion, "completion");
Function2 var3 = new <anonymous constructor>(completion);
var3.p$ = (CoroutineScope)value;
return var3;
}
public final Object invoke(Object var1, Object var2) {
return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
}
}), 3, (Object)null);
this.L$0 = $this$launch;
this.L$1 = b;
this.label = 1;
if (b.await(this) == var5) {
return var5;
}
break;
case 1:
// 7
b = (Deferred)this.L$1;
$this$launch = (CoroutineScope)this.L$0;
ResultKt.throwOnFailure($result);
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
// 8
String var4 = "async end";
System.out.println(var4);
return Unit.INSTANCE;
}
@NotNull
public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
Intrinsics.checkParameterIsNotNull(completion, "completion");
Function2 var3 = new <anonymous constructor>(completion);
var3.p$ = (CoroutineScope)value;
return var3;
}
public final Object invoke(Object var1, Object var2) {
return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
}
}), 3, (Object)null);
// 4
(new Handler()).postDelayed((Runnable)null.INSTANCE, 1000L);
少し長いですが、重要なポイントとそのステート マシンに関連する内容だけを見ていきます。
- 最初に出力し、
main start
次にlaunch
コルーチンを作成してコルーチン ステート マシンに入り、この時点でlabel
関連するロジック0
を実行します。case: 0
- 、 output 、 call 、および pass
case: 0
を入力して現在のコルーチンを中断し、中断プロセス中に現在の中断ポイントのデータを記録し、に設定します。**async start**
async
await
lable
1
- 作成したコルーチンを入力し
async
、このときasync
コルーチンにlable
、0
実行async case: 0
・dealy
中断するasync
コルーチンを入力します。label
に設定します1
。待って目が覚めた2s
。 - この時点で、コルーチンはすべて中断されます。つまり、コルーチン メソッドから飛び出し
launch
、handler
操作を実行します。post 1s
コルーチンよりもdealy
短いので、最初に出力し**main end**
、それを通過して1s
回復コルーチンの段階に入ります async
The coroutine in isdelay
resumed.のオブジェクトがdelay
method に渡されることに注意してくださいthis
。したがって、内部タイミングが完了すると、メソッドが呼び出されてコルーチン in が再開されます。つまり、メソッドが呼び出されます。async
Continuation
delay
2s
Continuation
resumeWith
async
invokeSuspend
- 中断される前に
async label
に設定されているため1
、 に入りcase: 1
、以前に中断されたサイトを再開し、例外をチェックして、最後に を返しますasync
。 - このとき
await
, 一時停止ポイントが復元されます. これも に渡されthis
, 対応するものはlaunch
途中にあることに注意してくださいContinuation
. メソッドはコールバックされます.そして最後にresumeWith
呼び出されます.マシーン。invokeSuspend
case 1:
- 最後に、引き続きoutput
async end
を実行すると、コルーチン操作が終了します。
上記のコードを実行して、出力が正しいことを確認できます
main start
async start
main end
async end
要約すると、コルーチンはsuspend
一時停止ポイントを識別するために使用されますが、実際の一時停止ポイントはCOROUTINE_SUSPENDED
それが戻るかどうかによって判断する必要があり、コードはステート マシンを介してコルーチンの一時停止と回復を反映します。中断する必要がある場合は、まずシーンを保持して次のステート ポイントを設定してから、メソッドを終了してコルーチンを中断します。中断中、現在のスレッドはブロックされません。対応するリカバリ パスがresumeWith
ステート マシンの次の状態に入ると同時に、次の状態に入るときに以前に中断されたシーンが再開されます。
この記事では、主にコルーチンの中断と再開の原理を紹介し、コルーチンのステート マシンに関連する実行プロセスについても分析します。コルーチンを学んでいる方の参考になれば幸いです。今後のコルーチン解析にご期待ください。
さらに Kotlin 学習教材が必要な場合は、QR コードをスキャンして無料で入手できます。
「史上最も詳細な Android 版 kotlin コルーチン エントリー アドバンスド コンバット 」
第 1 章 Kotlin コルーチンの基本の紹介
● コルーチンとは
● Job、Deferred、コルーチンのスコープとは
● Kotlin コルーチンの基本的な使い方
第2章 Kotlinコルーチンの重要な知識ポイントの予備説明
● コルーチンスケジューラ
● コルーチンのコンテキスト
●コルーチン起動モード
● コルーチンのスコープ
●サスペンド機能
第3章 Kotlin コルーチンの例外処理
●コルーチン例外の発生過程
● コルーチンの例外処理