Channel ↔ Flow/SharedFlow 互通与取舍

62 阅读5分钟

0) 心智模型速记

  • Channel的、点对点队列(单播)。有容量溢出策略(SUSPEND/DROP_*),可 close/cancel,适合工作队列/管线/Actor
  • Flow的按需序列(每个 collect 都从头跑)。默认无缓冲,逐元素串行;用 buffer/flowOn/*Latest 改执行/背压。
  • SharedFlow / StateFlow多播。SharedFlow(replay, extraBufferCapacity, onBufferOverflow);StateFlow = “总是最新” (像 CONFLATED + replay=1),不会完成

1) 互通 API 一览(怎么“接上/转过来”)

Channel → Flow

  • channel.receiveAsFlow():把 ReceiveChannel 暴露为 冷 Flow收集时开始从通道取;通道 close → Flow 完成。

  • channel.consumeAsFlow():同上,但收集结束时会取消通道(“消费型”)。仅在你“独占该通道”且收集完就应终止时使用;否则更常用 receiveAsFlow()。

Flow → Channel

  • flow.produceIn(scope, capacity = Channel.BUFFERED):在 scope 启一个协程收集 Flow,把元素送进一个 ReceiveChannel 并返回它。

  • 手工:flow.onEach { ch.send(it) }.launchIn(scope)(更显式,也能配 trySend 与限频/背压策略)。

Flow → SharedFlow / StateFlow

  • flow.shareIn(scope, started, replay):变 SharedFlow(热、多播)。started 常用 WhileSubscribed(timeout),replay 视需求(0~N)。

  • flow.stateIn(scope, started, initial):变 StateFlow(总是最新,有初始值)。

Channel → SharedFlow

  • 直接:channel.receiveAsFlow().shareIn(scope, WhileSubscribed(), replay=0);

  • 或手工收集:launch { for (e in ch) shared.emit(e) }。

SharedFlow/StateFlow → Flow/Channel

  • SharedFlow/StateFlow 本身就是 Flow(热)。

  • 要接 Channel:shared.produceIn(scope, capacity=...) 或 shared.onEach { ch.send(it) }.launchIn(scope)。

广播通道已废弃:BroadcastChannel/ConflatedBroadcastChannel → 请用 SharedFlow/StateFlow。


2) 背压/容量的“对应关系”(很重要)

目标语义Channel 配置Flow/SharedFlow 对应法
必达 + 真背压capacity=N, onBufferOverflow=SUSPEND(或 RENDEZVOUS)Flow 默认即“无缓冲逐元素”,或 buffer(N, SUSPEND);SharedFlow 用 replay=N, SUSPEND(所有订阅者都跟得上才不挂)。
只要最新CONFLATED(或 capacity=1 + DROP_OLDEST)StateFlow(天然“最新”);SharedFlow(replay=1, onBufferOverflow=DROP_OLDEST);Flow 链上用 conflate()。
滑窗保近capacity=32/64 + DROP_OLDESTSharedFlow(replay=0, extraBufferCapacity=32/64, onBufferOverflow=DROP_OLDEST);或 Flow 上 buffer(32, DROP_OLDEST)。
多播(一发多收)Channel 不适合(单播)SharedFlow/StateFlow。
单播工作池✅ Channel(多个消费者竞争同一队列)Flow/SharedFlow 不是天然单播;如需“每条只处理一次”,还是 Channel。

emit/send 挂起差异

  • Channel:是否挂起取决于容量×策略
  • SharedFlow:emit 也会挂起(当 replay+extraBufferCapacity 满、且慢订阅者未消费导致无空位时);tryEmit 非挂起。
  • Flow:默认逐元素串行;是否背压取决于有没有 buffer/flowOn 等边界。

3) 完成/错误/关闭 语义差异

  • Channel:close() 后不再接收新元素;接收方读完缓冲即结束;cancel() 直接终止(可能丢未读)。

  • Flow可完成/可失败;异常向下游传播。

  • SharedFlow/StateFlow不会完成/失败;结束要靠外部信号(例如特殊事件 Completed)或取消 scope。

    • 需要“带完成”的多播:用 事件类型携带完成/错误,或拆成“数据 SharedFlow + 状态 SharedFlow”。

4) 多播/单播的根本区别(避免错用)

  • Channel 是单播:每条消息只给一个接收者。你开 N 个消费者就是“工作池/抢占消费”。
  • SharedFlow 是多播:同一条数据同时给所有收集者;慢订阅者会滞后甚至在溢出时丢(取决于配置)。
  • 想“广播”:用 SharedFlow;想“排队分发”:用 Channel。

5) 场景选型矩阵(怎么选最顺手)

场景推荐
工作队列/流水线/限并发(一条任务只处理一次)Channel(SUSPEND 背压),配扇入/扇出/管线模式
UI 状态(总是最新)StateFlow(或 flow.stateIn(...))
UI/多订阅者事件(可能需要错过期间也能“补一条”)SharedFlow(replay=1, extraBufferCapacity=...)
高频行情/传感器(只要最新 + 限频)MutableSharedFlow(replay=0, extraBufferCapacity=64, DROP_OLDEST) + Flow 端 sample()/conflate()
将冷 Flow 变热并复用shareIn(scope, WhileSubscribed(...), replay)
将热流喂给老代码(阻塞式或选择器模型)shared.produceIn(scope) 得到 ReceiveChannel

6) 可抄用代码片段

A) Channel → Flow(并保留通道生命周期)

val ch: ReceiveChannel<Event> = ...
val flow: Flow<Event> = ch.receiveAsFlow()
flow.onEach(::handle).launchIn(scope)

B) Channel → Flow(消费即终止通道)

val flow = ch.consumeAsFlow() // 收集结束/取消时会 cancel channel
flow.collect { ... }

C) Flow → Channel(老组件只会收 Channel)

val out: ReceiveChannel<Item> = upstreamFlow.produceIn(scope, capacity = Channel.BUFFERED)
launch { for (i in out) legacyConsume(i) }

D) Flow → SharedFlow(热、多播、可重放)

val shared: SharedFlow<Item> =
  upstreamFlow
    .shareIn(
      scope = appScope,
      started = SharingStarted.WhileSubscribed(5_000),
      replay = 1 // 新订阅者先收到最近一条
    )

E) “只要最新”的多播:SharedFlow ≈ Conflated

val hot = MutableSharedFlow<State>(
  replay = 1,
  extraBufferCapacity = 0,
  onBufferOverflow = BufferOverflow.DROP_OLDEST // 与 replay=1 搭配呈“最新值”语义
)
// 更新
scope.launch { hot.emit(latestState()) }
// 收集(多处 UI)
hot.collect { render(it) }

F) Channel → SharedFlow(把单播源升级为多播)

val inbox: ReceiveChannel<Tick> = ...
val bus: SharedFlow<Tick> =
  inbox.receiveAsFlow()
       .shareIn(scope, SharingStarted.WhileSubscribed(5_000), replay = 0)

G) SharedFlow → Channel(给 select/通道架构用)

val sf: SharedFlow<Event> = ...
val ch: ReceiveChannel<Event> = sf.produceIn(scope) // 然后你可以 select { ch.onReceive { ... } }

7) 易坑与建议

  1. 把 Channel 当广播:会丢给大部分订阅者(单播特性)。广播请用 SharedFlow/StateFlow。
  2. 把 SharedFlow 当工作队列:一条事件会被每个订阅者处理一次,非你想要的“只处理一次”。队列请用 Channel。
  3. 忽略 SharedFlow 的背压:emit 也会因为慢订阅者而挂起;若不希望卡住生产者,用 extraBufferCapacity + onBufferOverflow=DROP_OLDEST 或 tryEmit。
  4. 随意 UNLIMITED:通道/SharedFlow 大容量会隐藏压力→高延迟/内存爆。容量从 64 起步,监控再调。
  5. 完成/关闭误解:SharedFlow/StateFlow 不会完成;需要完结信号请自定义事件类型或关闭上游 Channel/取消 scope。
  6. consumeAsFlow 误用:它会在收集结束时取消通道;当通道还会被别人用时不要用它,换 receiveAsFlow。
  7. 冷→热复用忘了 started 策略:shareIn 默认 Eagerly 可能过早启动;多数 UI 场景用 WhileSubscribed(timeout)。

一句话总结

  • 单播队列/工作池:Channel;多播状态/事件:SharedFlow/StateFlow;一次性管道或需要按需计算:Flow。
  • 互通很简单:channel.receiveAsFlow()、flow.produceIn(scope)、flow.shareIn/stateIn();关键是背压/容量/完成语义要匹配你的业务(必达 vs 只要最新、多播 vs 单播)。