• トップ
  • ブログ一覧
  • 【Kotlin】【Coroutines】asyncの挙動を確認する
  • 【Kotlin】【Coroutines】asyncの挙動を確認する

    えばたん(エンジニア)えばたん(エンジニア)
    2023.06.01

    IT技術

    はじめに

    Android Coroutines Async の挙動を確認したのでメモとして残しておく

    1. 準備
    2. まずは基本
    3. asyncとsuspend関数を組み合わせたときの実行順番
    4. 特殊な実装をした場合
    5. 例外を絡めた挙動

    1.準備

    環境情報

    1// Coroutines
    2dependencies {
    3    <省略>
    4    def coroutines_android_version = '1.6.0'
    5    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_android_version"
    6    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_android_version"
    7    <省略>
    8}

    suspend関数作成

    まずはasyncで動作させるsuspend関数を作成

    1suspend fun mySuspend(name: String, delayTimeMillis: Long): Int {
    2    return withContext(Dispatchers.Default) {
    3        log.emit("---------start mySuspend() $name---------")
    4        var result = 0
    5        (1..5).forEach {
    6            result += (1..100).random()
    7            delay(delayTimeMillis)
    8            log.emit("mySuspend() $name - $it")
    9        }
    10        log.emit("---------finish mySuspend() $name---------")
    11        return@withContext result
    12    }

    例外発生用のsuspend関数作成

    例外を発生させる関数も作っておく

    1@Suppress("UNREACHABLE_CODE")
    2suspend fun error(name: String, crashTime: Int = 5): Int {
    3    withContext(Dispatchers.Default) {
    4        log.emit("---------start error() $name---------")
    5        (crashTime downTo 0).forEach {
    6            if (it <= 0) {
    7                log.emit("throw Exception!")
    8                throw Exception("error")
    9            }
    10            log.emit("throw Exception まで あと${it}秒")
    11            delay(1000L)
    12        }
    13        log.emit("---------finish error() $name---------")
    14    }
    15    return (1..100).random()
    16}

    ログ表示用のLiveData用意しておく

    TextViewにログを表示するために実装(必須ではない)

    1val log = MutableSharedFlow<String>(replay = 100)
    2
    3val logTextLiveData: LiveData<String> = log.mapLatest {
    4    Log.d("my_log", it)
    5    val stringBuilder = StringBuilder()
    6    log.replayCache.forEach {
    7        stringBuilder.append(it)
    8        stringBuilder.append("\n")
    9    }
    10    stringBuilder.toString()
    11}.asLiveData()

    2.まずは基本

    asyncを使わず普通にsuspendを呼び出す

    2つのsuspend関数を直列に処理する

    1launch {
    2    val a = mySuspend("A", 1000L)
    3    val b = mySuspend("B", 300L)
    4    log.emit("a = $a")
    5    log.emit("b = $b")
    6}

    ログを確認すると

    mySuspend() A完了後にmySuspend() Bが実行されている

    1---------start mySuspend() A---------
    2mySuspend() A - 1
    3mySuspend() A - 2
    4mySuspend() A - 3
    5mySuspend() A - 4
    6mySuspend() A - 5
    7---------finish mySuspend() A---------
    8---------start mySuspend() B---------
    9mySuspend() B - 1
    10mySuspend() B - 2
    11mySuspend() B - 3
    12mySuspend() B - 4
    13mySuspend() B - 5
    14---------finish mySuspend() B---------
    15a = 233
    16b = 325

    普通にAsyncを使ってみる

    2つのsuspend関数をそれぞれasyncで囲んで、それぞれawaitで取得する

    1launch {
    2    val aDeferred = async { mySuspend("A", 1000L) }
    3    val bDeferred = async { mySuspend("B", 300L) }
    4    val a = aDeferred.await()
    5    val b = bDeferred.await()
    6    log.emit("a = $a")
    7    log.emit("b = $b")
    8}

    ログを確認すると

    mySuspend() AmySuspend() B並列で実行されている

    1---------start mySuspend() A---------
    2---------start mySuspend() B---------
    3mySuspend() B - 1
    4mySuspend() B - 2
    5mySuspend() B - 3
    6mySuspend() A - 1
    7mySuspend() B - 4
    8mySuspend() B - 5
    9---------finish mySuspend() B---------
    10mySuspend() A - 2
    11mySuspend() A - 3
    12mySuspend() A - 4
    13mySuspend() A - 5
    14---------finish mySuspend() A---------
    15a = 130
    16b = 316

    3. asyncとsuspend関数を組み合わせたときの実行順番

    2つのawaitの間で別のsuspendを実行

    1launch {
    2    val aDeferred = async { mySuspend("A", 300L) }
    3    val bDeferred = async { mySuspend("B", 1000L) }
    4    val a = aDeferred.await()
    5    val c = mySuspend("C", 700L) // ←2つのawaitの間で実行
    6    val b = bDeferred.await()
    7    log.emit("a = $a")
    8    log.emit("b = $b")
    9    log.emit("c = $c")
    10}

    ログを確認すると

    mySuspend() AmySuspend() Bが並列で実行され、mySuspend() A完了を待ってからmySuspend() Cが実行されている(mySuspend() BとmySuspend() Cは並列で実行される)

    1---------start mySuspend() A---------
    2---------start mySuspend() B---------
    3mySuspend() A - 1
    4mySuspend() A - 2
    5mySuspend() A - 3
    6mySuspend() B - 1
    7mySuspend() A - 4
    8mySuspend() A - 5
    9---------finish mySuspend() A---------
    10---------start mySuspend() C---------
    11mySuspend() B - 2
    12mySuspend() C - 1
    13mySuspend() C - 2
    14mySuspend() B - 3
    15mySuspend() C - 3
    16mySuspend() B - 4
    17mySuspend() C - 4
    18mySuspend() B - 5
    19---------finish mySuspend() B---------
    20mySuspend() C - 5
    21---------finish mySuspend() C---------
    22a = 295
    23b = 330
    24c = 284

    2つのasyncの間で別のsuspendを実行

    1launch {
    2    val aDeferred = async { mySuspend("A", 1000L) }
    3    val b = mySuspend("B", 700L) // ←2つのasyncの間で実行
    4    val cDeferred = async { mySuspend("C", 300L) }
    5    val a = aDeferred.await()
    6    val c = cDeferred.await()
    7    log.emit("a = $a")
    8    log.emit("b = $b")
    9    log.emit("c = $c")
    10}

    ログを確認すると

    mySuspend() AmySuspend() B並列で実行され、mySuspend() B完了を待ってからmySuspend() Cが実行されている(mySuspend() AmySuspend() Cは並列で実行される)

    1---------start mySuspend() B---------
    2---------start mySuspend() A---------
    3mySuspend() B - 1
    4mySuspend() A - 1
    5mySuspend() B - 2
    6mySuspend() A - 2
    7mySuspend() B - 3
    8mySuspend() B - 4
    9mySuspend() A - 3
    10mySuspend() B - 5
    11---------finish mySuspend() B---------
    12---------start mySuspend() C---------
    13mySuspend() C - 1
    14mySuspend() A - 4
    15mySuspend() C - 2
    16mySuspend() C - 3
    17mySuspend() C - 4
    18mySuspend() A - 5
    19---------finish mySuspend() A---------
    20mySuspend() C - 5
    21---------finish mySuspend() C---------
    22a = 341
    23b = 331
    24c = 338

    4. 特殊な実装をした場合

    普通やらないやり方で実装したとき、どのような挙動になるか?

    2つのasyncを実行し、片方のawaitを呼ばない

    1launch {
    2    val aDeferred = async { mySuspend("A", 1000L) }
    3    val bDeferred = async { mySuspend("B", 1500L) }
    4    val a = aDeferred.await()
    5    log.emit("a = $a")
    6    log.emit("b = bDeferred.await() を書かない")
    7}

    ログを確認すると

    mySuspend() B完了を待たずに、launchのブロックを最後まで実行されている(launchブロック完了後も残りを実行し続ける)

    1---------start mySuspend() A---------
    2---------start mySuspend() B---------
    3mySuspend() A - 1
    4mySuspend() B - 1
    5mySuspend() A - 2
    6mySuspend() B - 2
    7mySuspend() A - 3
    8mySuspend() A - 4
    9mySuspend() B - 3
    10mySuspend() A - 5
    11---------finish mySuspend() A---------
    12a = 306
    13b = bDeferred.await() を書かない
    14======== launchブロックはここまで ========15mySuspend() B - 4 // ←launchのブロックが終了後もBの残りを実行し続ける
    16mySuspend() B - 5 // ←launchのブロックが終了後もBの残りを実行し続ける
    17---------finish mySuspend() B---------

    1つ目のasyncをlaunch前に実行して、launch内部でawaitする

    1val aDeferred = async { mySuspend("A", 1000L) } // ←asyncをlaunch前に実行する
    2launch {
    3    val bDeferred = async { mySuspend("B", 1500L) }
    4    val a = aDeferred.await()
    5    val b = bDeferred.await()
    6    log.emit("a = $a")
    7    log.emit("b = $b")
    8}

    ログを確認すると

    mySuspend() Alaunch前に開始されている

    1---------start mySuspend() A--------- // ←launch前に開始されている
    2// ↓ここからlaunch内部
    3---------start mySuspend() B---------
    4mySuspend() A - 1
    5mySuspend() B - 1
    6mySuspend() A - 2
    7mySuspend() B - 2
    8mySuspend() A - 3
    9mySuspend() A - 4
    10mySuspend() B - 3
    11mySuspend() A - 5
    12---------finish mySuspend() A---------
    13mySuspend() B - 4
    14mySuspend() B - 5
    15---------finish mySuspend() B---------
    16a = 198
    17b = 280

    5.例外を絡めた挙動

    長くなるので次の記事に記載します。(むしろここからが本番です)

    えばたん(エンジニア)

    えばたん(エンジニア)

    おすすめ記事