LoginSignup
4
1

More than 3 years have passed since last update.

Coroutines私的メモ1~launch, runBlocking, scope, exception handling~

Last updated at Posted at 2019-11-27

これは私的な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あたりを触れていきたいです

参考

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1