開発

Kotlin Coroutinesとは?suspendの仕組みと構造化並行性・採用判断

Kotlin Coroutinesは、スレッドを占有したまま待たずに処理を中断・再開できる並行処理の仕組みです。言語側が用意するのはsuspend修飾子とContinuationという土台だけで、launchFlowといった道具はkotlinx.coroutinesライブラリが提供します。安定版はKotlin 1.3(2018年10月29日)から、ライブラリの最新安定版は1.11.0(2026年5月8日公開)。この記事では中断が成立する仕組み、ビルダーの書き分け、構造化並行性によるキャンセルの伝わり方を押さえたうえで、Java仮想スレッドが使える環境でもコルーチンを選ぶ条件まで整理します。

まとめ:Kotlin Coroutinesの採用判断と使いどころ

AndroidアプリをKotlinで書くなら、非同期処理の既定はコルーチンで構いません。ライフサイクルに紐づいたviewModelScopeが用意されており、画面が破棄された時点で通信もDBアクセスも自動で止まります。コールバックやRxJavaで組んでいた処理を、上から下へ読める逐次コードのまま書けるのが実装上の差です。

サーバーサイドのJVMだけが対象なら、判断は割れます。JDK 24でsynchronizedによるピン留めが解消され、Javaの仮想スレッドはブロッキングIOをそのまま流しても詰まりにくくなりました。既存のJava資産が多く、やりたいことがIO待ちのスループット改善に尽きるなら、仮想スレッドで足ります。

それでもコルーチンが優位に立つ条件は3つあります。Android端末上で動かす、iOSとコードを共有する、そしてストリーム処理をキャンセル込みで組み立てる。このいずれかに当たるなら、コルーチンを選ぶ理由が残ります。逆にJVMサーバー単体でCPUバウンドなタスクを並べるだけなら、素のスレッドプールで済ませて構いません。

Kotlin Coroutinesの定義とsuspendが中断を実現する仕組み

コルーチンがスレッドを占有せずに中断と再開を切り替える仕組み

コルーチンは「中断できる計算」の単位です。通常のスレッドは待ち状態に入るとOSスレッドをそのまま抱え込みますが、コルーチンは中断ポイントでスレッドを手放し、再開時に空いているスレッドへ載り直します。1スレッドの上に数万個のコルーチンを並べても破綻しないのは、待っている間にスレッドを消費していないからです。

ここで混同しやすいのが「軽量スレッド」という説明です。コルーチン自体はスレッドではなく、スレッドの上で動く状態を持ったオブジェクトにすぎません。どのスレッドで動くかを決めるのはディスパッチャ側の仕事で、コルーチン本体はスレッドの所属を持ちません。

suspend修飾子がコンパイル時にContinuationへ変換される流れ

中断の実体はコンパイラによる変換です。suspend funはコンパイル時に、末尾へContinuationを1つ受け取る通常の関数へ書き換えられます。関数の本体は中断ポイントごとに区切られた状態機械へ展開され、再開のたびに「どこまで進んだか」を示すラベルから続きが実行されます。

この変換はCPS変換と呼ばれる手法に沿ったもので、ランタイムに特別な仕掛けは要りません。だからJVMにもNativeにもJavaScriptにも同じ考え方で載せられます。生成されたバイトコードを読むと、ローカル変数がフィールドへ退避され、switch相当の分岐で再開位置が選ばれている構造が見えます。

実装上の帰結は明快です。suspendが付いた関数はコルーチンの中か、別のsuspend関数からしか呼べません。この制約があるおかげで、中断しうる箇所がシグネチャに現れます。

kotlinx.coroutinesが言語機能と分けて提供する範囲

言語仕様に含まれるのはsuspend修飾子、Continuation、そしてsuspendCoroutineのような最小限のプリミティブだけです。launchasyncDispatchersFlowChannelはいずれもkotlinx.coroutinesという別ライブラリの提供物で、依存に加えなければ使えません。

この分離は版管理の面で効いてきます。Kotlin本体は2.4.0が2026年6月、2.4.10が同年7月14日と進む一方、ライブラリは1.11.0が独立した周期で更新されます。言語だけ上げてライブラリを据え置く運用も成立するため、ビルドファイルでは両方の版を明示しておく。言語側の仕様はKotlinの特徴とJavaとの違いを整理した解説で押さえておくと、変換結果の読み解きが速くなります。

提供元 含まれるもの 版の例
Kotlin言語 suspend・Continuation 2.4.10
kotlinx.coroutines launch・Flow・Dispatchers 1.11.0
プラットフォーム別成果物 Dispatchers.Mainの実装 Android向けなど

launchとasyncで書き分けるコルーチンの起動と待ち合わせ

launchとasyncの戻り値の違いから決まる使い分けの基準

起動の入口は2つです。launchはJobを返し、結果を持ち帰らない処理に使います。asyncはDeferredを返し、awaitで値を受け取る前提の処理に使います。判断軸は「呼び出し側が戻り値を必要とするか」の一点で、必要ないならlaunchが既定です。

並列に走らせて両方の結果を待つ場面ではasyncを2つ並べ、そのあとでawaitします。順番を誤ってasyncの直後にawaitを書くと、2つ目の起動が1つ目の完了後になり、並列化した意図が消えます。

val user = async { api.fetchUser(id) }
val orders = async { api.fetchOrders(id) }
render(user.await(), orders.await())

withContextでディスパッチャを切り替えるメインセーフの作法

Androidの公式ガイドが示す作法は、呼び出し側ではなく関数側でスレッドを切り替える形です。ブロッキングを含む処理は関数の内側でwithContext(Dispatchers.IO)に包み、どのスレッドから呼ばれてもUIを止めないようにします。これをメインセーフと呼びます。

呼び出し側に切り替えを書かせる設計だと、1箇所でも書き漏らした瞬間にメインスレッドが固まる。責務を関数の内側へ寄せておけば、利用側はviewModelScope.launchから素直に呼ぶだけで済みます。withContextは結果を返して合流するため、逐次の読み口も壊れません。

runBlockingを使ってよい場所と本番コードで避ける理由

runBlockingは現在のスレッドをブロックしてコルーチンの完了を待ちます。用途はエントリポイントとテストに限られ、アプリの実行経路に混ぜると中断の利点を打ち消します。UIスレッドで呼べば、そのままフリーズにつながる。

ライブラリ1.11.0ではrunBlockingsuspendCancellableCoroutineにcallsInPlaceのコントラクトが加わり、ブロック内で代入した変数をコンパイラが確定として扱えるようになりました。書き心地は改善しましたが、使ってよい場所が広がったわけではありません。同期と非同期の境界そのものを整理したい場合は非同期処理と同期処理の違いをまとめた解説を先に読むと判断が早くなります。

構造化並行性がキャンセルと例外伝播をまとめて扱う設計の仕組み

CoroutineScopeとJobの親子関係が決めるキャンセルの伝わり方

コルーチンは必ずスコープの中で起動します。スコープが持つJobは起動した子コルーチンのJobを親として束ね、親をキャンセルすれば子も全て止まります。この親子関係が構造化並行性と呼ばれる仕組みで、起動しっぱなしのタスクが残らない仕掛けです。

逆向きの伝播にも明確な規則がある仕組みです。子が例外で失敗すると、その失敗は親へ伝わり、親は残る兄弟をキャンセルしてから自身も失敗します。「1つ落ちたら全部止める」が既定の振る舞いだと理解しておくと、後述するSupervisorJobの位置づけが見えます。

協調的キャンセルでensureActiveを挟む必要がある場面

キャンセルは強制停止ではありません。中断関数はキャンセル状態を検査してCancellationExceptionを投げますが、中断を含まない計算はキャンセル要求を受け取っても走り続けます。画像処理のような重いループを回すコードでは、この差がそのまま止まらないタスクとして表面化します。

対処は3つのいずれかです。ループの先頭でensureActive()を呼ぶ、isActiveを条件に入れる、あるいはyield()を挟んで他のコルーチンへ譲る。どれも1行で済むうえ、入れておかないとキャンセル済みのスコープでCPUを掴み続けます。

while (isActive) {
    step()
    ensureActive()
}

SupervisorJobとcoroutineScopeで変わる例外の伝播範囲

子の失敗を隔離したい場面ではSupervisorJobを使います。supervisorScopeの中で起動した子は、失敗しても兄弟を巻き込みません。画面上の3つのウィジェットがそれぞれ独立したAPIを叩くような構成では、1つの失敗で全体を白紙にしない意味があります。

対するcoroutineScopeは全て成功して初めて次へ進む合流点です。どちらを選ぶかは「部分的な失敗を許すか」で決まります。なお1.11.0では、コルーチンビルダーの引数にJobを直接渡す書き方が非推奨としてlintで示されるようになりました。Jobを渡すと親子関係が意図せず切れるため、スコープを分けて表現する形へ寄せます。

構文 子が失敗したとき 向く場面
coroutineScope 兄弟も全てキャンセル 全件そろって次へ進む
supervisorScope 失敗した子だけ止まる 部分的な失敗を許す
SupervisorJob スコープ単位で隔離 常駐スコープの土台

Dispatchersの既定値とFlowで扱う非同期ストリームの基礎

DefaultとIOのスレッド数の既定値と実務で使い分ける基準

ディスパッチャはコルーチンを載せるスレッドを決めます。ライブラリ1.11.0のソースで確認できる既定値は、Dispatchers.DefaultがCPUコア数(最低2)、Dispatchers.IOが64かコア数の大きいほうです。前者はCPUバウンドな計算、後者は待ち時間が支配的なIOに割り当てます。

実装の内側では両者がスレッドを共有しており、Defaultで走っている最中にwithContext(Dispatchers.IO)へ切り替えても、実際にはスレッドが変わらない場合があります。切り替えのコストを恐れて自前のスレッドプールを作る前に、この事実を押さえておく。特定の接続数に合わせて上限を絞りたい場合はlimitedParallelismでビューを作れば、IO側の枠とは別枠で並列度を決められます。

Dispatchers.MainはJVMでは実装が同梱されず、Android向けやSwing向けの成果物を依存に加えて初めて解決されます。ここを入れ忘れると、実行時に例外で落ちる形で気づくことになります。

FlowとStateFlow・SharedFlowを使い分ける3つの判断軸

単発の値ではなく連続した値を扱うならFlowです。Flowはコールドで、収集を始めた時点で初めて生産が走ります。対してStateFlowとSharedFlowはホットで、収集者の有無に関わらず値が流れる点が違います。

判断軸は3つに整理できます。現在値を1つ保持して画面へ映したいならStateFlow、履歴を持たずイベントとして配りたいならSharedFlow、生産と消費が1対1で完結するならFlow。1.11.0ではSharedFlowをFlowとして扱うasFlowや、収集結果をMapへ集める終端演算子が追加され、変換の記述量が減りました。

Androidのライフサイクル連動スコープで漏れを防ぐ実務作法

AndroidではviewModelScopelifecycleScopeが用意されており、それぞれViewModelの破棄と画面の破棄でキャンセルされます。自前でCoroutineScopeを作る場面はほとんどありません。Flowを画面で収集するときはrepeatOnLifecycleで囲み、バックグラウンドへ回った間は収集を止めます。

サーバーサイドでも考え方は同じで、リクエスト単位のスコープに紐づける形が基本です。Ktorはこの設計を全面に採っており、実際の組み立て方はKtorの非同期設計とコルーチン基盤の解説で具体例が追えます。

Java仮想スレッドやGoroutineと比べた採用条件と見送り場面

Java仮想スレッド導入後もコルーチンを選ぶ3つの実務判断条件

Javaの仮想スレッドはJDK 21で正式機能となり、JDK 24のJEP 491でsynchronizedによるピン留めが解消されました。ブロッキング前提のコードをそのまま流してもキャリアスレッドが詰まりにくくなり、JVMサーバーでは「非同期のために書き換える」動機が薄れています。ただしJNIやFFM経由のネイティブ呼び出しでは依然ピン留めが残ります。

それでもコルーチンを選ぶ条件は次の3つです。Android端末上で動かす場合、iOSとロジックを共有する場合、そしてキャンセルと合流を構造として表現したい場合。特に3つ目は仮想スレッドに対応物がなく、親子関係でまとめて止める設計は自前で組む必要があります。Java仮想スレッドとProject Loomの仕組みを解説した記事と突き合わせると、どちらの制約が案件に効くか判断できます。

Kotlin 2.4のSwift exportで広がったKMPでの適用範囲

Kotlin 2.4ではSwift exportがアルファとして入り、suspend関数をSwiftのasync関数として、FlowをAsyncSequenceとして書き出せるようになりました。iOS側から見ると、Kotlinで書いた非同期ロジックが素のSwift構文で呼べる形になります。

共有できる範囲が広がった意味は小さくありません。従来はKMPで共有したロジックをiOS側へ渡す際、コールバックへ畳んで受け渡す層が要りました。その中間層が減る分、共有コードの保守対象も減ります。アルファ段階のため本番採用は段階的に進める前提ですが、AndroidとiOSで非同期の設計をそろえたい案件では検討の材料になります。

コルーチンを見送り既存スレッドプールで済ませてよい実務の判断条件

見送ってよい場面もはっきりしています。JVMサーバー単体で完結し、処理がCPUバウンドで、タスク数が数百程度に収まる構成。この条件では、ExecutorServiceに投げるだけで足ります。コルーチンを入れると依存とビルド設定が増える分、得るものが釣り合いません。

既存のRxJavaコードが安定して動いているケースも、無理に移す判断は勧めません。移行の価値が出るのは、コールバックの入れ子が3段を超えている、キャンセル処理が手書きで散らばっている、Kotlin Multiplatformへ広げる予定がある、のいずれかに当たるときです。判断を先送りしたまま部分的に混在させると、キャンセルの経路が二重になって不具合の切り分けが難しくなります。

コルーチン実装でつまずく失敗パターンと保守体制を決める判断軸

GlobalScopeとJob渡しが招くキャンセル漏れの典型パターン

最も多い詰まり方はGlobalScopeでの起動です。アプリの生存期間と同じスコープなので、画面を閉じても通信が続き、破棄済みのビューを触って落ちます。ライブラリ側もDelicateCoroutinesApiとして注記しており、通常の実装で選ぶ理由はありません。

同種の失敗がコルーチンビルダーへのJob渡しです。launch(Job())のように書くと親子関係が切れ、スコープをキャンセルしても子が止まらなくなります。1.11.0からはlintで非推奨として示されるため、依存を上げた時点で既存コードの該当箇所が洗い出せます。

CancellationExceptionを握り潰して起きる不具合の見つけ方

キャンセルはCancellationExceptionという例外で運ばれます。ここをcatch (e: Exception)で受けて握り潰すと、キャンセルしたはずの処理が続行し、リソースの解放も走りません。runCatchingも同じ罠を持ちます。

回避策は決まっています。catch節の先頭でensureActive()を呼び、キャンセルなら再スローする。あるいはCancellationExceptionを明示的に受けて投げ直す形にします。この一手間を省くと、再現条件が「画面を素早く閉じたとき」に限られる不具合になり、切り分けに時間を取られます。

try {
    load()
} catch (e: Exception) {
    currentCoroutineContext().ensureActive()
    report(e)
}

Androidアプリの非同期設計を外注へ任せるときの引き継ぎ基準

非同期の設計は、コードを読むだけでは正しさを判定しにくい領域です。キャンセル漏れもディスパッチャの誤選択も、平常時は動いてしまう。だから外部へ任せる場合は、成果物の受け入れ基準を契約時に文章で決めておく必要があります。

引き継ぎの基準は3つに絞れます。スコープの生成箇所がライフサイクルに紐づいていること、ブロッキング処理を含む関数がメインセーフになっていること、キャンセル時の挙動がテストで担保されていること。Androidアプリ開発の受託では既存アプリの非同期処理の棚卸しから対応しており、RxJavaからの移行やキャンセル経路の整理も含めて相談できます。

よくある質問

Kotlin Coroutinesの導入でよく挙がる質問を、実装と発注の両面から5つ整理します。

コルーチンとスレッドは何が違いますか?

スレッドはOSが管理する実行単位で、待機中もリソースを抱えます。コルーチンは中断できる計算の単位で、待機時にスレッドを手放せる仕組みです。1つのスレッドの上で多数のコルーチンが交代しながら動くため、同時に扱える待機タスクの数が桁違いになります。ただしCPUを使い切る計算そのものは速くなりません。

GlobalScopeは使ってよいですか?

通常の実装では使いません。GlobalScopeで起動したコルーチンはアプリが終わるまでキャンセルされず、画面遷移後も走り続けます。ライブラリ側もDelicateCoroutinesApiとして注記しており、使う場合は明示的な同意が要る設計です。AndroidならviewModelScopeやlifecycleScope、サーバーならリクエスト単位のスコープを使います。

Dispatchers.IOには何を流せばよいですか?

ファイル読み書き、DBアクセス、そして中断に対応していないブロッキングなネットワーク呼び出しです。既定の並列度は64かコア数の大きいほうなので、接続数に上限があるDBへ流す場合はlimitedParallelismで枠を切ります。逆に画像のデコードや集計のようなCPUバウンドな処理はDispatchers.Defaultへ回します。

コルーチンのテストはどう書きますか?

kotlinx-coroutines-testのrunTestを使います。仮想時間で動くため、delayを含む処理も待ち時間なしで検証可能です。Dispatchers.Mainに依存するコードはDispatchers.setMainでテスト用ディスパッチャに差し替えます。なおディスパッチャを直接参照せず引数で受け取る設計にしておくと、差し替えの手間が減ります。

Javaのコードからコルーチンを呼べますか?

suspend関数をJavaから直接呼ぶことはできません。JVM向けのkotlinx-coroutines-coreに含まれるfutureビルダーでCompletableFutureへ変換するか、Kotlin側でコールバックを受け取る通常の関数として公開します。Javaが主体のプロジェクトへ部分導入する場合は、この境界をどこに引くかを先に決めておきます。

関連記事

資料請求

RELATED POSTS 関連記事