Help us understand the problem. What is going on with this article?

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

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

参考

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした