これは私的なCoroutinesに関するメモです。
今回はlaunch, runBlocking, join, supervisorScopeについて触れます。
もし指摘点ありましたら遠慮なくまさかり飛ばしてもらえればと思います。
Coroutinesとは
中断(suspend)と再開(resume)の機能によってスレッドよりも低コスト・可読性高く非同期処理を扱える
導入
Kotlin1.3でstableになりました。
dependencies {
def coroutines_version = "1.3.2"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
}
stableはこちらで確認できます
前提
現在のスレッドがわかるようにloggerメソッドを仕込むことにします
fun logger(msg: String) = println("${Thread.currentThread().name} : $msg")
GlobalScope.launch
GlobalScope
はコルーチンのスコープで、すべてのコルーチンはいずれかのスコープに紐づくことになっています。
fun normalLaunch() {
logger("start")
GlobalScope.launch {
logger("Hello")
}
logger("World")
}
試しにこのようなコードを書いた場合は
main : start
main : World
になります。launch
はその実行中のスレッド(今回はmain
)をブロックしないためです。
そのため処理したいならほんの少しだけスレッドを停止させてあげれば可能です。
fun normalLaunch() {
logger("start")
GlobalScope.launch {
logger("Hello")
}
Thread.sleep(100L)
logger("World")
}
実行結果はこのように
main : start
DefaultDispatcher-worker-1 : Hello
main : World
ただしアプリ開発やサーバサイド開発するうえで、このようなコードをずっと書いていくわけにもいかないと思います。
runBlocking
そこで、runBlocking
を使います。このブロックで囲まれた部分は現在のスレッドをブロックして実行します。
fun normalRunBlocking() {
logger("start")
runBlocking {
logger("Hello")
}
logger("World")
}
結果は
main : start
main : Hello
main : World
runBlockingのブロックが終了することを待ってから最後のloggerへ移ります。
では、コルーチン内で他のコルーチンを記述する場合はどうなるのでしょうか。(=親子関係)
Job#Join
子のGlobalScope.launch
で記述されたJob
に対してjoin()
することで親のコルーチンは子のコルーチンの終了を待機します。
変数宣言せずそのまま記述してもよいのですがこちらのほうが管理がずっと楽になりますね。
fun parentChild() {
val job = GlobalScope.launch {
delay(1000L)
logger("Hello")
}
logger("start")
runBlocking {
job.join()
logger("World")
}
logger("end")
}
結果としては
main : start
DefaultDispatcher-worker-1 : Hello
main : World
main : end
Child coroutinesの扱い
このようなコードを仕込んでみます
2つの子関係にあるコルーチンが存在しています
fun parentChildWhenException() {
suspend fun child1() {
delay(2000L)
logger("child1")
}
suspend fun child2() {
logger("child2")
delay(1000L)
throw RuntimeException("Exception!")
}
try {
logger("start")
runBlocking {
launch { child1() }
launch { child2() }
}
logger("end")
} catch (e: Exception) {
logger("${e.message}")
} finally {
logger("finally")
}
}
実行すると以下になります
main : start
main : child2
main : Exception!
main : finally
子関係にあるchild2()
側で1秒後に例外が投げられることで、child1()は実行されず親のコルーチンがキャンセルされます。
ではchildに対してExceptionをそれぞれ管理しつつきちんと実行されるにはどうしたらよいでしょう。
supervisorScope
childに関係なく実行させたい場合はsupervisorScope
を使います。試しに上記のコードにそれだけを挿入したコードを実行してみます。
fun supervisorScope() {
suspend fun child1() {
delay(2000L)
logger("child1")
}
suspend fun child2() {
logger("child2")
delay(1000L)
throw RuntimeException("Exception!")
}
try {
logger("start")
runBlocking {
supervisorScope {
launch { child1() }
launch { child2() }
}
}
logger("end")
} catch (e: Exception) {
logger("${e.message}")
} finally {
logger("finally")
}
}
結果としては
main : start
main : child2
Exception in thread "main" java.lang.RuntimeException: Exception!
at com.example.todoappsandbox.CoroutinesKt$supervisorScope$2.invokeSuspend(Coroutines.kt:76)
at .. (略)
main : child1
main : end
main : finally
のようになり、child1自体も実行されるようになりました。一方catchされずExceptionが挙げられてしまいました。supervisorを使う場合は子の例外が親に伝播されないためです。そのため、子自身に例外処理を定義する必要があります。
CoroutineExceptionHandler
CoroutineExceptionHandler
を使って例外を定義できます。
それを対象の子のコルーチンに挿入して対応ができます。(launch(handler)
)
fun supervisorScopeHandleChildException() {
suspend fun child1() {
delay(2000L)
logger("child1")
}
suspend fun child2() {
logger("child2")
delay(1000L)
throw RuntimeException("Exception!")
}
val handler = CoroutineExceptionHandler {_, e ->
logger("Handle Child Exception: ${e.message}")
}
try {
logger("start")
runBlocking {
supervisorScope {
launch { child1() }
launch(handler) { child2() }
}
}
logger("end")
} catch (e: Exception) {
logger("${e.message}")
} finally {
logger("finally")
}
}
これで例外なく全ての子関係のコルーチンが実行できるようになりました。
main : start
main : child2
main : Handle Child Exception: Exception!
main : child1
main : end
main : finally
次回はdispatchers, withContext, async/awaitあたりを触れていきたいです