2024/11/04 Kotlinのコルーチンを勉強する
Kotlin
Kotlinのコルーチン
もう記事があった🤯t6o_o6t.icon
😎iNoma.icon 
Pythonのasyncioは経験があるt6o_o6t.icon
runBlocking
asyncio.runのようにブロックしてコルーチンを実行する存在?
suspend関数
Pythonにおけるコルーチン関数のような存在?
CoroutineScope.launchはsuspend関数を最後の引数に受け取る
Trailing Lambdaで関数を渡すとsuspend関数と推論される?
CoroutineScope.launchはJobを返す
Job.joinで明示的に終了を待つことができる
await task?
asyncやlaunchはCoroutineScopeの拡張関数
CoroutineScopeのblock内で使用する
asyncとlaunchのシグネチャの違い
launchはJobを、asyncはDefferedを返す
Defferedとは、結果を持つJobである
job.cancelでJobの実行をキャンセルできる
挙動を理解するには構造化された並行性(Structured Concurrency)の理解も必要だろう
https://ja.wikipedia.org/wiki/構造化された並行性
Reactなどのフレームワーク利用者からKotlinコルーチンの特徴がどう見えるのか気になるiNoma.icon
似ているものと比較して差異をみることで本質が浮かび上がる
Kotlinのコルーチンの実行の過程をイメージするには、PythonのasyncioやJavaScriptのasync / awaitのことは一度忘れて考えなければならない
code:bad.py
async def f() {
# ...
}
async def main() {
feature = f()
# Pythonではコルーチン関数の戻り値を await することで待機できたが..
await feature
}
code:good.kt
suspend fun f() {
# ...
}
suspend関数とは結局何か
suspend関数は、suspend関数を呼び出したときに一時停止する関数である
中断(suspend)と再開(resume)が可能な関数
実行順序はコルーチンスコープ内での記述通りに保ちつつ、非同期処理を可能にする関数
中断・再開を繰り返しながら実行できるため、時間のかかる処理でもスレッドをブロッキングしない
他のコルーチンと並行して実行可能
suspend関数も上から順に実行されていく
ルール
suspend関数を呼び出すと、現在のコルーチンの実行が一時停止する
一時停止状態が解除されるのは、suspend関数の呼び出しが完了するときである
下記のコードではどのような順番で実行されるだろうか?
code:order.kt
import kotlinx.coroutines.*
fun main() = runBlocking {
doWorld()
}
suspend fun doWorld() = coroutineScope { // (A)
println("1")
val job = launch { // (B)
delay(100L) // <-- suspend関数の呼び出し!
println("2")
}
println("3")
job.join() // <-- suspend関数の呼び出し!
println("4")
}
実行の過程は次の通りである。
1. A) println("1")
2. A) val job = launch { ... }
launchに渡したblockをもとに、コルーチンが起動する
3. 以下が並行的に実行される
A
1. println("3")
2. job.join()
suspend関数の呼び出し!行(A)で起動されたコルーチンの実行は、一時停止する。
一時停止が解除されるのは、job.join()の呼び出しが完了するとき、すなわちjobが完了するときである。
ref. 2024/11/04 Kotlinのコルーチンを勉強する#67374efa8458750000a4505f
B
1. delay(100L)
suspend関数の呼び出し!行(B)で起動されたコルーチンの実行は、一時停止する。
一時停止が解除されるのは、delay(100L)の呼び出しが完了するとき、すなわち100msの待機が終わるときである。
2. println("2")
この2つが並行的に実行されるとどうなるだろうか?
AとBそれぞれの順序関係は保って実行される。たとえば、Bにおいてprintln("2")が実行されるのは、delay(100L)が完了したときだけだ。
したがって、出力は3, 2となる。
4. A) job.join()呼び出しの一時停止状態が解除される。
注:実際にはこの処理も、上述の並行処理の一部として行われる。
5. A) println("4")
したがって、次のように表示される
1
3
2
4
coroutineScopeはsuspend、launchは通常の関数だ
先のコードで、job.join()の呼び出しをlaunchで囲ってみよう。結果はどうなるだろうか?
code:order_2.kt
println("3")
launch {
job.join()
}
println("4")
結果は、3の直後に4が表示され、最後に2が表示される。
launchは通常の関数なので、行(A)で起動されたコルーチンの実行は一時停止しない。
したがって、println("4")の実行を妨げるものはないので、2よりも4のほうが先に表示される。
では、job.join()の呼び出しをcoroutineScopeで囲うとどうなるだろうか?
まとめ
suspend関数の呼び出しをlaunchで囲うと、
coroutineScopeをcoroutineScopeの中で使うことには意味を感じなかった。
coroutineScopeはsuspend関数なので、内部のcoroutineScopeの完了を待機しなければならない。
しかし、内部のcoroutineScopeの呼び出しは、coroutineScopeによって実行される処理が終わらなければ完了しない。
つまり、結局coroutineScopeで囲んだ処理が完了しなければ、呼び出し側の処理を再開することはできないのである。
asyncも考え方はいっしょ
code:async.kt
import kotlinx.coroutines.*
fun main() = runBlocking {
val deffered = async {
delay(100L)
"<body>Hello World</body>"
}
println("Connecting...")
val resp = deffered.await()
println("Success!")
println(resp)
}
出力
Connecting...
Success!
<body>Hello World</body>