Yappli Tech Blog

株式会社ヤプリの開発メンバーによるブログです。最新の技術情報からチーム・働き方に関するテーマまで、日々の熱い想いを持って発信していきます。

suspendCoroutine内で非同期処理を行う

こんにちは、Androidエンジニアの 近藤(ふなち / Hunachi) です☺︎

最近は、DroidKaigiが開催されたり勉強会がたくさん開かれたり賑やかな日々ですね🎉

今回は、suspendCoroutineを使うコードを書いている中でそうだったのか!と思った挙動があったため紹介します♡

suspendCoroutineとは

kotlinlang.org

超大雑把にいうとresumeの内容を非同期で返すsuspend関数です。Callback処理をCoroutine化したい時によく使用されると思います。delay関数内でも使われてますね。

この記事では、このsuspendCoroutine内で非同期処理を書きたい場合に知っておきたいことについて紹介していきます。

suspendCoroutine内で非同期処理がしたい!

どんな状況なのか?

生活していると、Callback内のnot suspend関数内で非同期処理したくなることもあると思うんです。

例えば、

suspend fun suspendCoroutineTest(): String = suspendCoroutine { continuation ->
    val callback = object: HogeCallback {
        override fun onHoge() {
            // ここで非同期処理がしたい
        }
    }
}

こんな感じです。

これをCoroutineで実現しようと思うと以下のように書けると思います。(一例です。)

suspend fun suspendCoroutineTest(): String = suspendCoroutine { continuation ->
    // 親のScopeがキャンセルされた時にキャンセルされて欲しいため
    val coroutineScope = CoroutineScope(continuation.context)
    val callback = object: HogeCallback {
        override fun onHoge() {
            coroutineScope.launch {
                // 非同期処理
                delay(1000)
                // ここでresumeすることも可能⭕️
                continuation.resume("finish")
            }
        }
    }
}

もっと詳しく挙動を見てみる

今までのコードでは想像しやすいようにCallbackを使ってましたがCallbackが挟まるとごちゃごちゃするので、とりあえずシンプルな挙動がわかりやすいコードを書いてみました。

以下のコードはどのような結果になると思いますか?

私は勘違いしていたので一発で当てられませんでした😅
右上の緑色の三角ボタンを押し動かしてみて答えを確認していただけると嬉しいです。

コードを少し詳しく見てみます。

1. 0.25秒後に親スコープはキャンセルされる

main関数の部分でparentScopeをキャンセルしています。

つまり最低でも0.25秒後にはsuspendCoroutineTestの中の子スコープであるchildScopeもキャンセルされます。

よって (1 .. 5).forEach の部分は最高でもit = 2の状態までしか到達しません😭

2. suspendCoroutineTest内の出力される順番
suspend fun suspendCoroutineTest(): String = suspendCoroutine { continuation ->
    println("start suspendCoroutineTest") // → 1番
 val childScope = CoroutineScope(continuation.context)
    // 別スレッドになるので一旦無視できる
    childScope.launch {
        (1 .. 5).forEach {
            delay(100)
            println(it) // → (実行されるなら…)4番
        }
    }
    continuation.resume("call resume") // → 3番(suspendCoroutineのreturnが実行されたらこの値が戻り値になる)
    println("resumed suspendCoroutineTest")  // →2番
}
3. childScope.launch内部はいつまで生きている?

resumeに関係なく、parentScopeがキャンセルまで生き続けていました。
私はこの部分を知りませんでした。
つまり2の4番の部分は実行されます!

結果を再確認

1、2、3を考慮すると結果は、

start suspendCoroutineTest
resumed suspendCoroutineTest
call resume
1
2

になります👏

余談

resumeのタイミングで非同期処理も止めたい場合はどうすればいいの?

明示的にキャンセルしてあげましょう!

continuation.resume("call resume")
childScope.cancel()
suspendCoroutineと似た関数にsuspendCancellableCoroutineもあるよ

キャンセルしなきゃいけないJobだったり、解放しなきゃいけないオブジェクトがある際は、suspendCoroutineの代わりにsuspendCancellableCoroutineを使うと便利です。

まとめ

suspendCoroutine内で非同期処理を行う際は、resumeをしただけでは内部の非同期処理や後続処理は止まらないことに注意しましょう!

最後に

ヤプリにご興味がある方いましたら是非カジュアル面談でヤプリ社員と話しませんか? 

open.talentio.com