一般社団法人 全国個人事業主支援協会

COLUMN コラム

  • Kotlinのコルーチン入門:非同期処理をシンプルに書く方法

なぜKotlinコルーチンが注目されるのか

非同期処理はモダンなアプリケーション開発において避けて通れないテーマです。ネットワーク通信、ファイルI/O、データベースアクセスなど、時間のかかる処理をメインスレッドで実行するわけにはいきません。Javaの世界ではコールバック、RxJava、CompletableFutureなど様々なアプローチがありましたが、いずれもコードの複雑さという課題を抱えていました。

Kotlinのコルーチンは、この問題に対するエレガントな解決策です。非同期処理を同期的なコードのように書けるという特徴を持ち、コードの可読性と保守性を大幅に向上させます。Android開発では事実上の標準となっており、サーバーサイドKotlinでも広く採用されています。

コルーチンの基本概念

コルーチンを理解するために、まず基本的な概念を整理しましょう。

suspend関数

コルーチンの中核となるのが suspend キーワードです。この修飾子を付けた関数は、実行を一時停止し、後で再開できることを示します。

suspend fun fetchUserData(userId: String): User {
val response = httpClient.get("https://api.example.com/users/$userId")
return response.body()
}

この関数は一見すると通常の同期関数と同じように見えますが、suspend キーワードにより、HTTP通信の間スレッドをブロックせずに待機できます。

CoroutineScope

コルーチンは必ずスコープ内で起動する必要があります。スコープはコルーチンのライフサイクルを管理し、適切なタイミングでキャンセルする仕組みを提供します。

import kotlinx.coroutines.*

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

runBlocking はコルーチンスコープを作成し、内部のコルーチンが全て完了するまで現在のスレッドをブロックします。主にテストやmain関数で使用します。

コルーチンビルダーの使い分け

コルーチンを起動するための主要なビルダーは3つあります。それぞれの特徴を理解して使い分けましょう。

launch:結果を返さない非同期処理

val scope = CoroutineScope(Dispatchers.Default)

scope.launch {
val users = fetchAllUsers()
updateCache(users)
println("キャッシュ更新完了: ${users.size}件")
}

launchJob オブジェクトを返します。戻り値が不要な非同期処理、いわゆる「Fire and Forget」パターンに適しています。

async:結果を返す非同期処理

val scope = CoroutineScope(Dispatchers.Default)

val deferredUser = scope.async {
fetchUserData("user-123")
}
val deferredOrders = scope.async {
fetchUserOrders("user-123")
}

// 両方の結果を待つ
val user = deferredUser.await()
val orders = deferredOrders.await()
println("${user.name}の注文数: ${orders.size}")

asyncDeferred オブジェクトを返し、await() で結果を取得できます。複数の非同期処理を並行実行して結果を集約する場合に威力を発揮します。

withContext:コンテキストの切り替え

suspend fun loadImage(url: String): Bitmap {
return withContext(Dispatchers.IO) {
val bytes = downloadBytes(url)
decodeBitmap(bytes)
}
}

withContext は指定したディスパッチャーでブロック内の処理を実行し、結果を返します。I/O処理をIOディスパッチャーに切り替える際によく使われます。

Dispatcherの理解

コルーチンがどのスレッドで実行されるかを決定するのがDispatcherです。適切なDispatcherの選択はパフォーマンスに直結します。

  • Dispatchers.Main:UIスレッドで実行。UI更新に使用(Android専用)
  • Dispatchers.IO:I/O操作に最適化されたスレッドプール。ネットワーク通信やファイルアクセスに使用
  • Dispatchers.Default:CPU負荷の高い処理用。JSONパースや画像処理などに使用
  • Dispatchers.Unconfined:特定のスレッドに束縛されない。テスト用途以外ではほぼ使わない

構造化された並行性

Kotlinコルーチンの大きな特徴が「構造化された並行性(Structured Concurrency)」です。これは、コルーチンの親子関係を明確にし、リソースリークを防ぐ設計思想です。

suspend fun fetchDashboardData(): DashboardData = coroutineScope {
val userDeferred = async { fetchUser() }
val statsDeferred = async { fetchStats() }
val newsDeferred = async { fetchNews() }

DashboardData(
user = userDeferred.await(),
stats = statsDeferred.await(),
news = newsDeferred.await()
)
}

coroutineScope 内で起動した全てのコルーチンが完了するまで、この関数は完了しません。もしいずれかのコルーチンで例外が発生した場合、他のコルーチンも自動的にキャンセルされます。この仕組みにより、非同期処理の管理が格段に容易になります。

エラーハンドリング

コルーチンにおけるエラーハンドリングは、通常のtry-catchで行えます。

suspend fun safeFetchUser(id: String): Result<User> {
return try {
val user = fetchUserData(id)
Result.success(user)
} catch (e: HttpException) {
println("HTTP エラー: ${e.code}")
Result.failure(e)
} catch (e: CancellationException) {
throw e // キャンセル例外は再スロー必須
}
}

重要な注意点として、CancellationException は絶対にキャッチしてはいけません。これをキャッチしてしまうと、コルーチンのキャンセル機構が正しく動作しなくなります。上記のように、キャッチした場合は必ず再スローしてください。

実務での活用ポイント

最後に、実務でコルーチンを活用する際のポイントをまとめます。

  • ViewModelScope/lifecycleScopeを活用:Android開発では、ライフサイクルに紐づいたスコープを使うことでメモリリークを防止できます
  • Flowとの組み合わせ:リアクティブなデータストリームが必要な場合は、Kotlin Flowを併用する
  • テストではrunTestを使用:コルーチンのテストには kotlinx-coroutines-testrunTest を使い、仮想時間でdelayをスキップする
  • グローバルスコープは避けるGlobalScope はライフサイクル管理ができないため、原則として使用しない

コルーチンは最初こそ概念の理解に時間がかかりますが、一度身につければ非同期処理のコードが劇的にシンプルになります。まずは小さなプロジェクトで試してみることをお勧めします。

この記事をシェアする

  • Twitterでシェア
  • Facebookでシェア
  • LINEでシェア