UGA Boxxx

つぶやきの延長のつもりで、知ったこと思ったこと書いてます

【Kotlin】コルーチンとは

Kotlinにコルーチン(Coroutine)という非同期処理を簡単かつ効率的に行うための仕組みがあることを知った

コルーチンという言葉に馴染みがなかったので調べたが、非同期処理を実装するための機能で、中断可能なノンブロッキングな非同期処理を簡潔に記述できるものらしい

以下に調べたKotolinのコルーチンについてまとめる

1. コルーチンの基本概念

  • 軽量: コルーチンは非常に軽量で、同時に大量のコルーチンを実行できる。通常のスレッドよりもリソース消費が少なく、コンテキストスイッチのオーバーヘッドもほぼない
  • 協調的な停止: コルーチンは必要に応じて実行を一時停止し、他のコルーチンにCPU時間を譲る。このため、非同期処理や並列処理が効率的に実現される
  • サスペンド関数: コルーチンはsuspendというキーワードを持つ関数を使って、非同期処理を実現する。suspendは、関数が実行を一時停止し、後で再開できることを示す

2. 基本的なコルーチンの使い方

Kotlinでは、kotlinx.coroutinesライブラリを使用してコルーチンを扱う

このライブラリを使用するために、build.gradleに以下を追加する必要がある

dependencies {
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2"
}

例: コルーチンの基本的な実行

import kotlinx.coroutines.*

fun main() {
    // コルーチンの開始
    GlobalScope.launch {
        delay(1000L)  // 1秒待機(非ブロッキング)
        println("World!")
    }

    println("Hello,")
    Thread.sleep(2000L)  // メインスレッドが終了しないようにする
}

出力:

Hello,
World!
  • GlobalScope.launch: 新しいコルーチンを起動する。launchは非同期に処理を実行する
  • delay(1000L): コルーチンを1秒間一時停止するが、スレッドをブロックしない
  • Thread.sleep(2000L): メインスレッドを2秒間停止させることで、World!の出力を待つ。これがないと、メインスレッドが先に終了してしまう

3. コルーチンの主要な構造

1. launch: ジョブ(Job)を返す非同期処理

launchは新しいコルーチンを起動し、そのコルーチンは独立して実行される

処理が終了するまで待つ必要がない

GlobalScope.launch {
    // 非同期処理
}

2. async: Deferredを返す非同期処理

asyncは非同期処理を開始し、その結果を待つことができる

戻り値はDeferredオブジェクトで、これはawait()を使って結果を取得できる

val deferred = GlobalScope.async {
    // 非同期処理
    return@async 42
}

val result = deferred.await()  // 非同期処理が終了するまで待ち、結果を取得
println(result)  // 42

3. suspend: サスペンド関数

suspendキーワードを使うことで、その関数内でコルーチンを一時停止し、後で再開できるようになる

サスペンド関数は他のサスペンド関数や非同期処理でしか呼び出すことができない

suspend fun doSomething() {
    delay(1000L)  // サスペンド関数内でコルーチンを一時停止
    println("Done!")
}

4. コルーチンの実行コンテキスト

Kotlinのコルーチンには、どのスレッドで実行されるかを指定するコルーチンディスパッチャがある

主なディスパッチャは以下の通り

  • Dispatchers.Default: CPU集約型の処理に使用されるスレッドプール上で実行される
  • Dispatchers.IO: I/O操作に最適化されており、ファイルやネットワークの入出力操作に使う
  • Dispatchers.Main: Androidのメインスレッドで実行し、UI更新などに使う
  • Dispatchers.Unconfined: 最初に呼び出されたスレッドでコルーチンを開始し、次に継続するスレッドは任意とする

例: Dispatchersを使ったコルーチン

fun main() = runBlocking {
    launch(Dispatchers.Default) {
        // CPU集約型の処理
        println("Running in Default dispatcher")
    }

    launch(Dispatchers.IO) {
        // I/O集約型の処理
        println("Running in IO dispatcher")
    }
}

5. runBlockingについて

runBlockingは、コルーチンをブロッキングで実行するための関数。通常は、メイン関数やテストケースのような場面で使う

runBlockingは呼び出し元のスレッドをブロックし、コルーチンの完了を待つ

fun main() = runBlocking {
    launch {
        delay(1000L)
        println("World!")
    }

    println("Hello,")
}

この例では、runBlockingを使ってコルーチンが完了するまでメインスレッドをブロックする

6. エラーハンドリング

コルーチン内で例外が発生した場合、例外をキャッチしてエラーハンドリングする必要がある

try-catchを使用してエラーハンドリングが可能

fun main() = runBlocking {
    val job = launch {
        try {
            delay(1000L)
            throw Exception("Something went wrong!")
        } catch (e: Exception) {
            println("Caught an exception: ${e.message}")
        }
    }
    job.join()
}

7. コルーチンのキャンセル

コルーチンはcancel()を使ってキャンセルすることができる

コルーチン内で定期的にisActiveをチェックすることで、キャンセルを適切に処理できる

fun main() = runBlocking {
    val job = launch {
        for (i in 1..5) {
            if (!isActive) return@launch
            println("Processing $i")
            delay(500L)
        }
    }
    
    delay(1200L)
    job.cancel()  // コルーチンをキャンセル
    println("Job cancelled")
}