Kotlinの非同期処理について

採用はこちら

Kotlinにおける非同期処理は、Android・サーバーサイド・CLIツールなど幅広い場面で使われます。

一方で、Coroutineは非常に柔軟な仕組みであるがゆえに、用語や挙動を誤解したまま使われやすいのも事実です。

本記事では、Kotlinの非同期処理について「正しい概念」「よくある誤解」「実務で安全な使い方」を中心に、精査済みの内容で解説します。

目次

なぜ非同期処理が必要なのか

同期処理(Blocking処理)は、処理が完了するまで呼び出し元のスレッドを占有します。

  • UIスレッドが止まり、画面が固まる
  • サーバーでスレッドが枯渇し、スループットが落ちる
  • 同時処理数が増えるほど性能が急激に悪化する

この「待ち時間」をスレッドで抱え込まず、裏で処理しつつ他の仕事を進めるために非同期処理が必要になります。

Kotlin以前の非同期処理の問題点

Thread / Executor

  • スレッド生成・管理のコストが高い
  • キャンセルや例外処理が難しい
  • ライフサイクル管理が困難

Callbackベース設計

  • ネストが深くなり可読性が低下
  • エラー処理が分散しやすい
  • 処理の流れが直感的に追えない

これらの問題を解決するために、KotlinではCoroutineが採用されました。

Coroutineとは何か

Coroutineの本質

  • 軽量な並行処理の仕組み
  • スレッドをブロックしない
  • 非同期処理を「同期処理のように」書ける

Coroutineは「別スレッドで勝手に動く仕組み」ではありません。

スレッド上で処理を一時停止・再開できる仕組みです。

suspend関数の正しい理解

suspendの意味

suspend は「この関数は中断・再開できる」という性質を表します。

  • suspend関数 = 非同期処理、ではない
  • suspend関数 = 中断可能な関数

実際に非同期になるかどうかは、

  • delay
  • 非同期I/O
  • withContext
    など、中で何をしているかによって決まります。

重要な制約

  • suspend関数は、別のsuspend関数かCoroutine内からのみ呼べる
  • suspendは「スレッドをブロックしない」ことを保証するための仕組み

CoroutineScopeとライフサイクル管理

CoroutineScopeとは

CoroutineScopeは「このコルーチンはいつまで生きてよいか」を決める単位です。

  • Scopeがキャンセルされると、配下のCoroutineもキャンセルされる
  • ライフサイクルと結びつけることでリークを防ぐ

主なScopeの使い分け

  • UI層:画面やViewModelの寿命に紐づける
  • サーバー:リクエスト単位・アプリ単位で管理
  • GlobalScope:基本的には使用しない(代替のスコープを持つ)

Dispatcher(スレッドの選択)

Dispatcherは「どの種類のスレッドで処理するか」を決めます。

  • Main:UI操作
  • IO:ネットワーク、DB、ファイルI/O
  • Default:CPU計算向け

注意点

  • UI層ではMainが前提のため、明示しなくても問題ない場合が多い
  • ブロッキングI/Oや重い処理は、IOやDefaultへ明示的に切り替える
  • Dispatcherを意識しない設計は、性能問題やUIフリーズの原因になりやすい

launch と async の違い

launch

  • 戻り値を持たない
  • 完了・キャンセル(Job)を管理したいときに使う

async

  • 戻り値を持つ(Deferred)
  • 処理結果を後で取得したい場合に使う

使い分けの本質

  • 結果が不要 → launch
  • 結果が必要 → async

並列処理かどうかよりも、「戻り値が必要か」で判断する方が安全です。

並行処理と構造化並行性

Kotlin Coroutineは構造化並行性を採用しています。

基本ルール

  • 親Coroutineがキャンセルされると子もキャンセルされる
  • 子で発生した例外は、原則として親に伝播する

これにより、

  • 例外の握り潰し
  • 処理の暴走
    を防げます。

補足

  • supervisorScopeSupervisorJob を使うと、子の失敗で兄弟を巻き込まない設計も可能
  • asyncの例外は await() 時に表面化する点に注意

キャンセルと例外処理

キャンセル

  • Coroutineは協調的にキャンセルされる
  • delay や suspendポイントでキャンセルが検知される

例外処理の注意点

  • launch:未捕捉例外は親スコープへ伝播しやすい
  • asyncawait() するまで例外が見えにくい
  • CoroutineExceptionHandler は主に launch 向け

Flowの正しい位置づけ

Flowは「時間とともに流れてくる値」を扱うためのAPIです。

特徴

  • 宣言的なストリーム処理
  • 変換・合成が得意
  • coldが基本(collectされるまで動かない)

StateFlow / SharedFlow

  • StateFlow:状態管理(常に最新値を保持)
  • SharedFlow:イベント・通知用途

Channelとの違い

FlowとChannelは用途が異なります。

  • Flow:ストリーム処理・データ変換に強い
  • Channel:送受信を明示する通信路(キュー的)

Channelは「再利用不可」ではありませんが、

  • 一度消費された値は再度流れない
  • Flowのように再collectで同じ値が再生されるわけではない

という点で、性質が異なります。

現代Kotlinでは、まずFlow(特にStateFlow / SharedFlow)を検討するのが一般的です。

runBlockingの正しい扱い

runBlocking は「Coroutine世界とBlocking世界をつなぐための仕組み」です。

適切な使用例

  • テストコード
  • CLIツールの main
  • 同期APIとの境界

避けるべき場所

  • AndroidのMainスレッド
  • サーバーのリクエスト処理

「テスト専用」ではありませんが、UIや高頻度処理では危険です。

よくある誤解と注意点

  • GlobalScopeの乱用は避ける(アプリスコープを用意する)
  • async + awaitを即時1回だけ使うなら、他の方法で十分な場合が多い
  • suspend = 自動的に非同期、ではない
  • Dispatcherを意識しないと性能問題を招きやすい

まとめ

  • Kotlinの非同期処理の中心はCoroutine
  • suspendは「中断可能」を表すだけ
  • ScopeとDispatcher設計が安全性と性能を決める
  • async / launch は「戻り値の有無」で選ぶ
  • ストリーム処理はFlowが第一選択
  • runBlockingは用途限定で使う

以上、Kotlinの非同期処理についてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次