一、
flatMapMerge(concurrency = N):把"元素→子流"并发执行并合并输出
语义
- 对上游每个元素 x,用变换函数生成一个 子 Flow:x -> Flow。
- 最多同时收集 N 个子流 (每个子流在独立协程 中运行),它们的结果交错合并 到下游(不保序)。
- 如果活跃子流已达 N,外层对后续元素的处理会挂起 ,直到有子流结束(这就是并发闸门)。
scss
ids.asFlow()
.flatMapMerge(concurrency = 8) { id ->
flow { emit(api.fetch(id)) } // 每个 id 一条子流
.flowOn(Dispatchers.IO) // 子流内部切到 IO
}
.collect { println(it) } // 结果交错到达
背压/取消/错误
-
背压 :当下游慢、合并缓冲满时,子流的 emit 会挂起 ;若外侧也满了,新元素进入会被阻塞。
-
取消 :下游取消 → 所有活跃子流一并取消。
-
错误 :任意一个子流抛异常 → 整个合并流失败、其它子流被取消(除非你在子流内部 try/catch 吃掉)。
何时用
-
"一批任务并发处理 + 合并结果",例如:
-
并发请求/解析:ids -> fetch(id)。
-
并发压缩/转码:files -> transcode(file).
-
并发数据库查询/本地计算。
-
和同族的区别
-
flatMapConcat:顺序收集每个子流(不并发,保序)。
-
flatMapLatest:新元素到来时取消旧子流(只保"最新一条"的处理)。
常用范式
1) "并发 map" 的等价写法(最实用)
kotlin
inline fun <T, R> Flow<T>.mapAsync(
concurrency: Int,
crossinline block: suspend (T) -> R
): Flow<R> =
flatMapMerge(concurrency) { t ->
flow { emit(block(t)) }
}
// 用法
ids.asFlow().mapAsync(8) { id -> api.fetch(id) }
2) 并发但保序输出(简易有序版)
注意:保序通常意味着牺牲一部分吞吐。
scss
data class Indexed<T>(val i: Int, val v: T)
val ordered: Flow<Result> =
ids.withIndex().asFlow()
.flatMapMerge(concurrency = 8) { (i, id) ->
flow { emit(Indexed(i, api.fetch(id))) }
}
.toList() // 若必须严格保序,需聚合后再排序(会失去流式)
.sortedBy { it.i }
.asFlow()
.map { it.v }
真正"边并发边保序且保持流式"需要更复杂的"有序缓冲"实现(可自写 mapAsyncOrdered)。
二、
channelFlow { ... }:用Channel搭一个"并发生产 → 单流输出"的自定义源
语义
- 是一个 Flow 构建器 ,内部有一个 ProducerScope(带 send/trySend),你可以在块内 launch 多个子协程并发 地往同一个 Channel 发送元素。
- channelFlow 会把这些元素按到达顺序 送给下游;是冷流 (collect 才启动),并遵守结构化并发(collect 结束/取消,会取消它内部的所有子协程)。
- 内部使用一个有界 Channel (实现通常相当于 Channel.BUFFERED),send 在满时会挂起(背压)。
kotlin
fun subscribeMarkets(matchId: String): Flow<OddsTick> = channelFlow {
val markets = listOf("1x2", "ahcp", "ou")
markets.forEach { m ->
launch(Dispatchers.IO) {
provider.subscribe(matchId, m) { tick ->
trySend(tick).isSuccess // 不阻塞版本,失败可按需丢弃或重试
}
}
}
// 结束条件:由下游取消,或你自行 close
// 不需要 awaitClose(callbackFlow 适配回调时才常用)
}
对比 callbackFlow :二者都基于 Channel。callbackFlow 面向"把单个回调源 适配成 Flow"(需要 awaitClose { ... } 解绑回调);channelFlow 更像"我来管多个并发生产者"。
背压/取消/错误
-
背压:send 满时挂起;trySend 立即返回失败让你自定丢弃策略(如只保最新)。
-
取消 :下游取消/完成 → channelFlow 作用域整体取消,子协程全停。
-
错误 :块内任一子协程未捕获异常 → 整个 channelFlow 失败;若要"一个失败不影响其他",在子协程里 try/catch 或用 supervisorScope。
何时用
- 一个元素需要扇出 为多条并发子任务/子订阅,再汇入同一条流(例如:一场比赛订阅多个盘口,多源合并)。
- 需要把外界主动推送(WebSocket、系统回调、多路监听)汇聚成 Flow。
- 想精细控制 "在块内自己起/停子协程" ,不适合用 flatMapMerge 时。
三、如何选:flatMapMerge vs channelFlow
场景 | 选哪一个 | 说明 |
---|---|---|
"每个输入元素"对应"一条子流/一次异步任务",并发处理后合并结果 | flatMapMerge(N) | 写法简单,天然有并发闸门(concurrency)。 |
需要在一个构建器内起多个并发生产者 ,或把多路回调/订阅汇入 | channelFlow | 你手动 launch/send,最灵活。 |
需要保序输出 | flatMapConcat 或自写有序缓冲 | 并发+保序要额外结构。 |
需要"最新优先,旧任务取消" | flatMapLatest / mapLatest | 不是合并,而是取消旧。 |
需要适配单个回调 API(注册/反注册) | callbackFlow | 兄弟 API,常配 awaitClose{}。 |
四、和flowOn/buffer的放置策略
-
子任务跑哪 :把重活写在 子流/子协程内部,然后对它们 flowOn(Dispatchers.IO/Default) 或 launch(Dispatchers.IO)。
-
边界缓冲:
- flatMapMerge 之后可接 .buffer(k) 给合并输出再加一层队列,削峰。
- channelFlow 外侧也可接 .buffer(k);内侧用 send/trySend 决定"阻塞还是丢弃"。
-
只要最新:在 UI 侧接 conflate() 或 buffer(1, DROP_OLDEST),保证高频场景不压爆渲染。
五、组合范式(可直接套)
1) 并发网络 + 合并(限并发 8)
scss
ids.asFlow()
.mapAsync(concurrency = 8) { id -> api.fetch(id) } // = flatMapMerge + flow{emit}
.buffer(256)
.flowOn(Dispatchers.IO)
.collect { render(it) }
2) 单输入扇出为多任务并发,再汇入(用channelFlow)
scss
fun enrich(userId: String): Flow<UserFull> = channelFlow {
val acc = AtomicReference(UserFull.empty(userId))
fun emitIfReady() {
acc.get().takeIf { it.allReady() }?.let { trySend(it) }
}
launch(Dispatchers.IO) {
val p = repo.profile(userId)
acc.updateAndGet { it.copy(profile = p) }
emitIfReady()
}
launch(Dispatchers.IO) {
val o = repo.orders(userId)
acc.updateAndGet { it.copy(orders = o) }
emitIfReady()
}
launch(Dispatchers.IO) {
val r = repo.risk(userId)
acc.updateAndGet { it.copy(risk = r) }
emitIfReady()
}
}
.conflate() // 只要最新聚合结果
.sample(100) // 限刷新频率
3) 推送聚合(多盘口/多源 → 一条 UI 流)
scss
fun oddsFlow(matchId: String): Flow<Odds> = channelFlow {
listOf("1x2","ahcp","ou").forEach { m ->
launch(Dispatchers.IO) {
provider.subscribe(matchId, m) { tick ->
// 背压下丢旧留新
trySend(tick).isSuccess
}
}
}
}
.buffer(256) // 外侧再加一段队列
.conflate() // UI 只要最新
.sample(200) // 帧率对齐
六、易坑与规约
- 保序误用 flatMapMerge :它不保序;要保序用 flatMapConcat,或自建有序缓冲。
- channelFlow 内忘记处理失败 :任一子协程异常会整流失败;关键子任务用 try/catch 或 supervisorScope。
- 无限并发:flatMapMerge 大并发 + 每个子流还起子协程,容易把线程/连接打满;务必控好 concurrency 与 dispatcher。
- 放错 flowOn :谁想在哪个线程跑,就把它写在对应 flowOn 的上游。在 flow{} 中 withContext 违规。
- 背压策略混乱 :UI 只需"最新"时,用 conflate()/DROP_OLDEST;统计/审计流需"必达",就用 SUSPEND 并持久化,拆两条管道。
- 在 channelFlow 里阻塞 send:若不想因为背压挂起生产者,请用 trySend 并结合外侧 buffer()/conflate() 设计丢弃策略。
结论
- 批量并发、元素级并行:优先 flatMapMerge(concurrency = N)(语义简洁、闸门好用)。
- 多源并发生产/汇聚、复杂扇出-汇入:用 channelFlow { ... }(你掌舵并发与发送时机)。
- 配合 flowOn/buffer/conflate/sample/*Latest,就能把吞吐、实时性、稳定性三者调到业务需要的平衡点。