気軽に楽しくプログラムと遊ぶ

自分が興味があってためになるかもって思う情報を提供しています。

Kotlin Coroutineの概念を理解する

以下でCoroutinの使い方をざっくり理解しました。
https://tamata78.hatenablog.com/entry/2021/09/06/174235

上記で理解を進める中でわからなかった概念を補足する情報を記載していきます。

CoroutineScope

  • CoroutineScopeは、コルーチンビルダー関数であるlaunchまたはasyncを使用して作成したコルーチンを全て追跡します。
  • 実行中のコルーチンはscope.cancel()を呼び出してキャンセル可能
  • キャンセルされたスコープは、コルーチンをそれ以上作成できません。
class ExampleClass {

    // Job and Dispatcher are combined into a CoroutineContext which
    // will be discussed shortly
    val scope = CoroutineScope(Job() + Dispatchers.Main)

    fun exampleMethod() {
        // Starts a new coroutine within the scope
        scope.launch {
            // New coroutine that can call suspend functions
            fetchDocs()
        }
    }

    fun cleanUp() {
        // Cancel the scope to cancel ongoing coroutines work
        scope.cancel()
    }
}

CoroutineScopeの生成時には、ジョブとスレッドプールであるディスパッチャを結合した値を設定する必要があります。

ジョブ

  • ジョブはコルーチンのハンドル(管理側)です。
  • 各コルーチンは、コルーチンのライフサイクルを管理するJobインスタンスを返却します。
  • Jobを用いてコルーチンをキャンセルするなどのライフサイクル管理ができます。
class ExampleClass {
    ...
    fun exampleMethod() {
        // Handle to the coroutine, you can control its lifecycle
        val job = scope.launch {
            // New coroutine
        }

        if (...) {
            // Cancel the coroutine started above, this doesn't affect the scope
            // this coroutine was launched in
            job.cancel()
        }
    }
}

CoroutineContext

コルーチンの設定のようなもの。 以下のCoroutineContext要素(設定項目)があります。

  • Job: コルーチンのライフサイクルを制御
  • CoroutineDispatcher: 適切なスレッドに処理を送信
  • CoroutineName: コルーチンの名前。デバッグに用いる
  • CoroutineExceptionHandler: キャッチされない例外を処理

上記の要素は以下のように割り当てられます。

  • 新規コルーチン作成時にJobインスタンスが割り当てられます。
  • Job以外のCoroutineContext要素は、コルーチンを含むスコープから継承されます。
  • 継承された要素を変更するには、launchasyncに変更対象要素を渡す必要があります。
class ExampleClass {
    val scope = CoroutineScope(Job() + Dispatchers.Main)

    fun exampleMethod() {
        // 新しいコルーチンの開始。ディスパッチャとコルーチン名をlanchの引数に設定すると変更できる
        val job = scope.launch(Dispatchers.Default + "BackgroundCoroutine") {
            // New coroutine with CoroutineName = "BackgroundCoroutine" (overridden)
        }
    }
}

+演算子は文字列結合とは違う処理なんでしょうか。引数二つ指定ではないのが少し疑問。。

ディスパッチャ

  • コルーチンは、ディスパッチャでコルーチンの実行スレッドを決定します。
  • DBアクセス、外部API実行などメインスレッドの外部で実行する場合は、デフォルト or IOディスパッチャを指定します。

開発者が指定できるディスパッチャは以下の3つになります。

  • Dispatchers.Main:UI を操作して処理を手早く作業する場合にのみ使用(suspend 関数の呼び出しなど)
  • Dispatchers.IO:メインスレッドの外部でディスクまたはネットワークの I/O を実行する場合に使用(ファイルの読み書き、ネットワーク オペレーションの実行など)
  • Dispatchers.Default :メインスレッドの外部で CPU 負荷の高い作業を実行する場合に使用(リストの並べ替えや JSON の解析など)

withContext()におけるディスパッチャ切り替え

  • withContext(Dispatchers.IO)を呼び出すことで、IOスレッドプールで実行するブロックを生成します。
  • get関数内は、IOディスパッチャ経由で実行されるようになります。
  • withContext自体が中断関数であるため、ディスパッチャのメインスレッドを中断して、get関数内の処理を実行します。
suspend fun fetchDocs() {                      // Dispatchers.Main
    val result = get("developer.android.com")  // Dispatchers.Main
    show(result)                               // Dispatchers.Main
}

suspend fun get(url: String) =                 // Dispatchers.Main
    withContext(Dispatchers.IO) {              // Dispatchers.IO (main-safety block)
        /* perform network IO here */          // Dispatchers.IO (main-safety block)
    }                                          // Dispatchers.Main
}

withContext()コルーチンを作成せず、ディスパッチャの切り替えのみを行うため、パフォーマンスに優れている。
withContextブロックの処理が完了するとget以降の処理が再開されて処理が継続されます。

実装のポイント

コールーチンの使いどころ

  • ループ処理内でのDBアクセスなど
  • 外部APIに後続処理を任せる場合。API実行を行った場合に、後処理を先にやらせておいて、レスポンスをその後、受け取るような実装。

実装時の注意点

実装していて、気づいた実装上の注意点を少し記載しておきます。

  • コルーチン生成を乱立すると逆にスレッド切り替えで遅くなる可能性があります
  • asyncのネストなどはできるだけ避ける。コルーチンが作られすぎでしまう
  • launch、runblockなどのコルーチンビルダー関数は、ループ内で入れず、外に記載。これもコルーチンを大量に作ってしまい、逆に性能劣化を招く

コルーチン内での例外

scope.launch内でのasyncをすると例外をキャッチできずにクラッシュするということが発生したりする。
この場合は、「coroutineScopeをtry catchで囲む」という方法で対処すると良い。

コルーチンがネストしていると親コルーチンに例外が伝播してしまうが、asyncをscope.launchと別のコルーチンで囲むことでscope.launchのコルーチンへ例外を伝播させるのを止めることができ、クラッシュを防ぐことができるようだ。

以下の記事の説明がわかりやすかった。
Coroutines asyncとException - Kenji Abe - Medium

参考URL