Channelとは:チャネルの種類とKotlinのコルーチン

著者: IT Sectr 公開日: 2026-03-17 読了時間: 8 分

Channelは、Kotlin Coroutinesライブラリの同期プリミティブで、コルーチン間でデータを転送するために使用されます。Kotlin Documentation, 2025によると、Channelはsuspend関数を介したブロッキング送信によりproducer-consumerパターンを実装します。ChannelはRendezvous、Buffered、Conflatedの各モードをサポートしており、それぞれがオーバーフロー時の動作を定義します。

重要なポイント

  • Channelはkotlinx.coroutinesのコルーチン間データ転送プリミティブで、producer-consumerパターンに基づいています
  • Rendezvous Channel — バッファなし:receive()が呼ばれるまでsend()は一時停止されます
  • Buffered Channel — 指定容量のバッファ付き、満杯になるとsend()が一時停止されます
  • Conflated Channel — 最新の値のみを保存し、オーバーフロー時に古い値は破棄されます
  • Channelはホットストリーム、callbackFlow、アクターモデル構築の基盤です

KotlinのChannelとは?

Channelは概念的にはJavaのBlockingQueueに似ていますが、ブロッキングするput()やtake()の代わりにsuspend関数のsend()とreceive()を使用します。Kotlin開発者は、共有メモリを介した同期なしでコルーチン間のデータ交換を組織化するためにChannelを使用します。チャネルは順序通りの配信を保証します — 送信順序は受信順序と一致します。

チャネルの作成

Channelを作成するには、ファクトリ関数Channel<T>(capacity)を呼び出します。capacityパラメータがチャネルのタイプを決定します:RENDEZVOUS (0)、UNLIMITED (Int.MAX_VALUE)、CONFLATED (-1)、または特定の数値。要素タイプTはジェネリクスで指定されます。close()によるチャネルのクローズは、新しい要素がもう来ないことを通知します。

SendとReceive

send(value)は、チャネルが満杯の場合に送信コルーチンを一時停止するsuspend関数です。receive()は、チャネルが空の場合に受信側を一時停止するsuspend関数です。代替のtrySend()とtryReceive()は非ブロッキングバージョンで、操作が不可能な場合にBooleanまたはnullを返します。これらは非suspendコンテキストで有用です。

kotlinx.coroutinesのChannelタイプ

Kotlinはバッファ容量によって4つのChannelバリアントを提供します:Rendezvous(容量0)、Buffered(容量N)、Conflated(容量1、上書き)、Unlimited(容量Int.MAX_VALUE)。各タイプは、厳密な同期から大量データのバッファリングまで、独自のタスクを解決します。

Rendezvous Channelは最も厳格です:別のコルーチンでreceive()が呼ばれるまでsend()はブロックされます。本質的には、2つのコルーチンのランデブーポイントです。送信者が受信者が要素を処理するのを待つ必要がある厳格なハンドシェイクに最適です。データ損失は排除されます — receiveが実行されるまでsendは完了しません。

Conflated Channelは最後に送信された値のみを保存します。受信者が古い値を取得する前に送信者が新しい要素を置いた場合、古い値は破棄されます。Conflated ChannelはUI状態に便利です:ユーザーがスライダーを素早く変更した場合、中間値は破棄され、最後の値のみが処理されます。

チャネルを使ったProducer-Consumer

Channelでの古典的なProducer-Consumerパターンは、並列コルーチンを通じて実装されます。Producerはループ内でsend(value)を呼び出し、consumerはreceive(value)を呼び出します。プロデューサーとコンシューマーは異なるDispatchersで動作できます:プロデューサーはDispatchers.IO、コンシューマーはDispatchers.Main。ChannelはLockやsynchronizedなしでアクセスを自動的に同期します。

Fan-out — 単一チャネル上の複数コンシューマー。各要素は正確に1つのコンシューマーに渡されます(ラウンドロビン分散)。Fan-in — 複数のプロデューサーが単一チャネルに書き込みます。送信コルーチンは送信を競合しますが、要素の順序は保持されます。どちらのシナリオも追加の同期は必要ありません。

Produceは、自動クローズ付きチャネルを作成するコルーチンビルダーです。produce { }関数はReceiveChannelを返します — コンシューマー用の読み取り専用チャネルです。ビルダー内でsend()がデータを送信し、ブロック完了時または例外発生時にチャネルは自動的にクローズされ、リークを防ぎます。

Selectと多重化

kotlinx.coroutinesライブラリはselectを提供します — 複数の選択肢の中から最初に完了したチャネルを待つ式です。Selectは複数チャネルの多重化を可能にします:例えば、2つのソースからのデータを待ち、最初に応答した方を処理します。構文 — select<T> { channel1.onReceive { } channel2.onReceive { } }。これはRxのamb演算子の代替です。

Channelコード例

最初の例は、送信者が受信を待つシンプルなRendezvous Channelです:

kotlin
val channel = Channel<String>()

scope.launch {
    channel.send("Hello")
    println("送信済み")
}

scope.launch {
    val msg = channel.receive()
    println("受信: $msg")
}

2番目の例 — 単一チャネル上の複数コンシューマー(fan-out):

kotlin
val channel = Channel<Int>(Channel.UNLIMITED)

scope.launch {
    for (x in 1..10) channel.send(x)
    channel.close()
}

repeat(2) { id ->
    scope.launch {
        for (msg in channel) {
            println("Consumer #$id: $msg")
        }
    }
}

3番目の例 — エラー処理付きproduceビルダーの使用:

kotlin
val source = produce {
    for (i in 1..5) {
        delay(200)
        send(i)
    }
}

scope.launch {
    source
        .consumeAsFlow()
        .catch { println("エラー: $it") }
        .collect { println("要素: $it") }
}

Channel vs Flow

Channelはホットプリミティブです:データはサブスクライバーとは無関係に発行されます。Flowはコールドです:データはサブスクリプション時に生成されます。Channelは各要素の1つのコンシューマーへの保証付き配信(fan-out)で複数のプロデューサーとコンシューマーをサポートします。Flowは複数の独立したプロデューサー向けに設計されていません。

Channelはバックプレッシャー管理のため、設定可能な容量のバッファとsuspend関数send/receiveを使用します。Flowはコルーチンによる自動バックプレッシャー付きのsuspendメカニズムcollectを使用します。Channelは特定のシナリオ向けの低レベルツールです:コールバック変換、アクターモデル、複数送信者を持つタスクキュー。

Androidでの日常的なシナリオ(UI状態、データベースからのリアクティブストリーム)では、GoogleはChannelではなくFlowを推奨しています。Channelは、正確なバッファ制御を伴うコルーチン間のホットデータ交換が必要な場合、または内部実装がChannelを使用するcallbackFlowを介してコールバックインターフェースを変換する場合に使用すべきです。

重要な実践例:WebSocketクライアントを実装する場合、Channelは1つのコルーチンからメッセージを書き込み、別のコルーチンから読み取ることを可能にし、各メッセージが正確に1回処理されることを保証します。Flowはコールドであり複数のプロデューサーをサポートしないため、このタスクには適していません。UNLIMITED容量のChannelは、一時的なコンシューマーの遅延中に受信メッセージが失われないことを保証します。

チャネルのライフサイクル管理はChannelでの作業の重要な部分です。すべてのデータが送信されたらチャネルをクローズして、コンシューマーが反復を完了できるようにする必要があります。channel.close()の呼び出しは、新しい要素がもう来ないことを通知します。コンシューマーはfor (item in channel)で反復できます — ループはclose()とバッファ枯渇後に自動的に終了します。代替として、コンシューマーはClosedReceiveChannelExceptionの処理とともにループ内でreceive()を呼び出すこともできます。

ChannelはAndroidで依存関係なしのEventBus実装に積極的に使用されています:Broadcast戦略を持つグローバルなChannel<Event>は、アプリケーションの任意のポイントからイベントを送信できます。LiveDataベースのバスとは異なり、Channelはライフサイクルに縛られず、画面遷移時にクリアする必要がありません。ViewModelからのsend()と、Activity/FragmentでのlifecycleScopeを介したreceive()は、Eventクラスなしで型安全な通信を提供します。Channel上の複数コンシューマーは負荷を分散します — 各要素は1回処理され、異なるサブスクライバーでの単一イベントの重複処理を防ぎます。

アクターシステムでは、Channelはメールボックス — アクターのメッセージキュー — を実装するための基盤として機能します。アクターはループ内でChannelからメッセージを読み取り、それらを順次処理するコルーチンです。このアプローチは、各メッセージがデータ競合なしに送信順序で処理されることを保証します。Kotlinには(Akkaとは異なり)組み込みのアクター型はありませんが、Channel + launchは軽量な代替手段です。

双方向交換にはチャネルペアが使用されます:1つはクライアントからサーバーへのリクエスト用、もう1つはサーバーからクライアントへのレスポンス用です。例えば、マルチスレッドアプリケーションでPipeを実装する場合:プロデューサーはOutputChannelに書き込み、コンシューマーはInputChannelから読み取ります。suspend関数sendとreceiveは、コルーチンがブロックではなく一時停止するため、Producer-Consumerがコールスタックをオーバーフローしないことを保証します。BUFFERED容量のChannelは、プロデューサーとコンシューマーの速度がほぼ等しいほとんどのシナリオに適しています。非対称シナリオではUNLIMITEDを使用して、コンシューマーがビジーのときにプロデューサーが一時停止しないようにします — これによりデッドロックのリスクは減少しますが、メモリ消費は増加します。

Channel容量の選択

チャネルでアーキテクチャを設計する際は、capacityを覚えておくことが重要です:容量の選択はピーク負荷時の動作に直接影響します。BUFFERED(N)容量のチャネルは平滑化バッファとして機能します:コンシューマーが一時的にプロデューサーより遅い場合、要素が蓄積されます。コンシューマーの平均速度が一貫してプロデューサーより低い場合、バッファは満杯になり送信コルーチンは一時停止されます — これはメモリ過負荷から保護する自動バックプレッシャーです。

Channelの監視とデバッグにはkotlinx-coroutines-debugを使用します:このユーティリティはアクティブなコルーチンの数、そのチャネルの状態(開/閉、バッファ内の要素数)、および一時停止されたsend/receive操作のコールスタックを表示します。Channelはロギングプロキシでラップすることもできます:LoggingChannel<T>クラスは実際のChannelに呼び出しを委譲し、send、receive、close操作をログ記録します。これは、close()が呼び出されずコンシューマーコルーチンが新しい要素を永遠に待っているチャネルリークを特定するのに役立ちます。

よくある質問

ChannelとBlockingQueueの違いは?

Channelはブロッキングするput()やtake()の代わりにsuspend関数send()とreceive()を使用します。BlockingQueueとは異なり、Channelはオーバーフロー時にスレッドをブロックしません — コルーチンが一時停止し、他のコルーチンのためにスレッドを解放します。これはKotlinでの効率的なスレッド使用に重要です。

閉じたChannelでsend()を呼ぶとどうなりますか?

閉じたチャネルでsend()を呼ぶと、ClosedSendChannelExceptionがスローされます。送信前にisClosedForSendを確認するか、閉じている場合にfalseを返すtrySend()を使用してください。close()は、既に送信された要素が例外スロー前に受信されることを保証します。

Conflated Channelはいつ使うべきですか?

Conflated Channelは、最新の状態のみが重要なイベントに便利です — プログレスバー、スライダー位置、タッチ座標など。コンシューマーがすべてのイベントを処理できない場合、中間のものは破棄され、最新のものが確実に処理されます。Conflated Channelのcapacityは-1です。

チャネルを閉じて残りの要素を処理するには?

channel.close()を呼び出します — チャネルは送信用に閉じられたとマークされますが、既に送信された要素はreceive()で読み取り続けられます。for (item in channel)での反復はバッファ枯渇後に自動的に終了します。isClosedForSendは即座にtrueを返し、isClosedForReceiveは枯渇後にtrueを返します。

ChannelをFlowで置き換えられますか?

常にではありません。Flowはコールドです — 1回のcollectにつき1回のエミッション。単一ストリームに書き込む複数の独立したプロデューサーが必要な場合、Channelが必須です。2つのコルーチン間の単純なデータ転送にはChannelを使用してください。データを伴うリアクティブストリームにはFlowを使用してください。

まとめ

  • Channelはコルーチン間でデータを転送するためのホット同期プリミティブです
  • Rendezvous — バッファなし、sendはreceiveが呼ばれるまでブロック
  • Buffered — 指定容量のバッファ付き、満杯でsendが一時停止
  • Conflated — 最新の値のみ保存、中間値は破棄
  • Produce — 自動クローズ付きチャネルのコルーチンビルダー
  • Fan-out — 複数コンシューマーがラウンドロビンで要素を分配
  • UI状態にはStateFlowを、Channelはホットキューとコールバック変換に使用

ターンキー方式のモバイルアプリケーションを開発します

IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。

プロジェクトについて相談

こちらもお読みください