非同期処理はモダンなアプリケーション開発において避けて通れないテーマです。ネットワーク通信、ファイルI/O、データベースアクセスなど、時間のかかる処理をメインスレッドで実行するわけにはいきません。Javaの世界ではコールバック、RxJava、CompletableFutureなど様々なアプローチがありましたが、いずれもコードの複雑さという課題を抱えていました。
Kotlinのコルーチンは、この問題に対するエレガントな解決策です。非同期処理を同期的なコードのように書けるという特徴を持ち、コードの可読性と保守性を大幅に向上させます。Android開発では事実上の標準となっており、サーバーサイドKotlinでも広く採用されています。
コルーチンを理解するために、まず基本的な概念を整理しましょう。
コルーチンの中核となるのが suspend キーワードです。この修飾子を付けた関数は、実行を一時停止し、後で再開できることを示します。
suspend fun fetchUserData(userId: String): User {
val response = httpClient.get("https://api.example.com/users/$userId")
return response.body()
}
この関数は一見すると通常の同期関数と同じように見えますが、suspend キーワードにより、HTTP通信の間スレッドをブロックせずに待機できます。
コルーチンは必ずスコープ内で起動する必要があります。スコープはコルーチンのライフサイクルを管理し、適切なタイミングでキャンセルする仕組みを提供します。
import kotlinx.coroutines.*
fun main() = runBlocking {
launch {
delay(1000L)
println("World!")
}
println("Hello,")
}
runBlocking はコルーチンスコープを作成し、内部のコルーチンが全て完了するまで現在のスレッドをブロックします。主にテストやmain関数で使用します。
コルーチンを起動するための主要なビルダーは3つあります。それぞれの特徴を理解して使い分けましょう。
val scope = CoroutineScope(Dispatchers.Default)
scope.launch {
val users = fetchAllUsers()
updateCache(users)
println("キャッシュ更新完了: ${users.size}件")
}
launch は Job オブジェクトを返します。戻り値が不要な非同期処理、いわゆる「Fire and Forget」パターンに適しています。
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}")
async は Deferred オブジェクトを返し、await() で結果を取得できます。複数の非同期処理を並行実行して結果を集約する場合に威力を発揮します。
suspend fun loadImage(url: String): Bitmap {
return withContext(Dispatchers.IO) {
val bytes = downloadBytes(url)
decodeBitmap(bytes)
}
}
withContext は指定したディスパッチャーでブロック内の処理を実行し、結果を返します。I/O処理をIOディスパッチャーに切り替える際によく使われます。
コルーチンがどのスレッドで実行されるかを決定するのがDispatcherです。適切なDispatcherの選択はパフォーマンスに直結します。
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 は絶対にキャッチしてはいけません。これをキャッチしてしまうと、コルーチンのキャンセル機構が正しく動作しなくなります。上記のように、キャッチした場合は必ず再スローしてください。
最後に、実務でコルーチンを活用する際のポイントをまとめます。
kotlinx-coroutines-test の runTest を使い、仮想時間でdelayをスキップするGlobalScope はライフサイクル管理ができないため、原則として使用しないコルーチンは最初こそ概念の理解に時間がかかりますが、一度身につければ非同期処理のコードが劇的にシンプルになります。まずは小さなプロジェクトで試してみることをお勧めします。