一些通用基石(先记住)
-
容量×策略:Channel(capacity = N, onBufferOverflow = SUSPEND | DROP_OLDEST | DROP_LATEST)
- “必达”流:SUSPEND;“最新优先/可丢旧”:DROP_OLDEST 或 CONFLATED。
-
非阻塞发送:trySend()(失败立即返回);“先 try 失败再挂起”的万能封装:
suspend fun <E> SendChannel<E>.offerOrSend(e: E) { if (!trySend(e).isSuccess) send(e) }
-
优雅收尾:生产结束用 close()(让消费把队列读干净),紧急终止用 cancel()。
-
资源清理:为持资源元素配置 onUndeliveredElement = { release(it) },避免丢弃/取消路径泄漏。
-
监控压力:自己维护 AtomicInteger 统计“入队-出队”长度,打点 p95/p99。
1) 管线(pipeline):阶段化处理,用多个通道串起来
何时用:典型 ETL/媒体/图像/音视频/日志处理,多阶段串联;每段可独立限速与并行。
核心要点****
-
每一段(Stage)只干一件事;段与段之间用 Channel 解耦。
-
每段的容量就是节流阀:队列小=低延迟;队列大=高吞吐但更“滞后”。
-
中间重活段可开 N 个 worker 并行消费。
代码范式
data class Job(val id: Int)
data class Mid(val id: Int, val data: ByteArray)
data class Out(val id: Int, val ok: Boolean)
val c12 = Channel<Job>(64, BufferOverflow.SUSPEND) // Stage1→2:必达
val c23 = Channel<Mid>(64, BufferOverflow.SUSPEND) // Stage2→3:必达
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
// Stage1:拉取/预处理
scope.launch(Dispatchers.IO) {
try {
source().forEach { c12.send(it) }
} finally { c12.close() }
}
// Stage2:CPU 重,开 4 个 worker
repeat(4) {
scope.launch(Dispatchers.Default) {
for (j in c12) {
val mid = transform(j) // 重计算
c23.send(mid)
}
}
}
// Stage3:落地/写盘/上传
scope.launch(Dispatchers.IO) {
try {
for (m in c23) sink(store(m))
} finally { c23.close() }
}
调优****
-
Stage2 爆满:加大 c12 容量或 worker 数;若允丢旧,可改 DROP_OLDEST。
-
端到端延迟大:先减小容量再评估并行度。
-
对“只要最新的展示”型中间态,可在 stage 边界前加 CONFLATED/DROP_OLDEST。
易坑****
- 无限容量/UNLIMITED 伪稳 → OOM。
- 只 cancel 生产者不 close 通道 → 消费者 for(e in ch) 不退出。
2) 扇入(fan-in):多个生产者 → 一个通道
何时用:多来源合流(多 socket/目录/传感器/任务队列)。
核心要点****
-
多个 launch 并发写同一 Channel;错误隔离用 SupervisorJob 或每个生产者内部 try/catch。
-
收尾:所有生产者完成后再 close 汇总通道(可用计数/joinAll)。
代码范式
val inbox = Channel<Event>(capacity = 256, onBufferOverflow = BufferOverflow.DROP_OLDEST)
val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
// 多个生产者
val producers = listOf(srcA(), srcB(), srcC())
producers.forEach { src ->
scope.launch {
try {
src.collect { e -> inbox.trySend(e) } // 高频:不阻塞生产者
} catch (t: Throwable) { /* log */ }
}
}
// 汇总消费者
scope.launch(Dispatchers.Default) {
try {
for (e in inbox) handle(e)
} finally { /* flush */ }
}
// 全部结束后关闭
scope.launch {
// 等待所有生产者结束(自行保存 Job 列表并 joinAll)
delay( /* …或 joinAll(producerJobs) … */ )
inbox.close()
}
调优****
-
高频场景(行情/日志)建议 DROP_OLDEST 或 CONFLATED;低频则 SUSPEND。
-
单消费者吃不动:在下一段做 pipeline + 多 worker。
易坑****
- 公平性不保证:快源可能“挤占”吞吐;必要时用**路由器(见扇出)**或为每源分独立通道再合并。
3) 扇出(fan-out):一个生产者 → 多个消费者(单播)
何时用:同一任务源,交给多 worker 并行处理(单播:一条消息只被其中一个消费)。
核心要点****
-
单个 Channel + N 个消费者:天然“竞争消费”(类似工作池),不保证均匀。
-
真正的“广播一条给所有人”→ 不用 Channel,请用 SharedFlow(或多通道复制)。
代码范式(工作池:竞争消费)
val jobs = Channel<Job>(capacity = 128, onBufferOverflow = BufferOverflow.SUSPEND)
// producer
launch(Dispatchers.IO) {
try { sourceJobs().forEach { jobs.send(it) } }
finally { jobs.close() }
}
// N workers(竞争消费)
repeat(8) {
launch(Dispatchers.Default) {
for (j in jobs) process(j) // 每个 j 只会被一个 worker 处理
}
}
均衡分发(Round-Robin 路由器)
val workerChs = List(4) { Channel<Job>(32, BufferOverflow.SUSPEND) }
val router = Channel<Job>(128, BufferOverflow.SUSPEND)
// 路由器:轮询分发
launch {
var idx = 0
for (j in router) {
workerChs[idx].send(j)
idx = (idx + 1) % workerChs.size
}
workerChs.forEach { it.close() }
}
// Worker
workerChs.forEach { ch ->
launch(Dispatchers.Default) { for (j in ch) process(j) }
}
调优****
-
源头高频但处理重:源→DROP_OLDEST,worker 通道 SUSPEND;或在 worker 前做限速/节流。
-
想要广播(每个消费者都要)→ 用 MutableSharedFlow(replay=0, extraBufferCapacity=...),不要用一个 Channel 给多人读。
易坑****
- 把 Channel 当广播:会丢给大部分消费者(因为单播)。
- 竞争消费下任务“偏食” :必要时加路由/按 key 分桶。
4) Actor(消息驱动):把共享可变状态封进单线程协程
何时用:需要一个**“串行的关键区”**来管理共享状态(计数器、缓存、会话、路由表),避免锁。
核心要点****
-
单协程 + 邮箱 Channel;所有状态变更都通过发送消息进入该协程。
-
严格串行(可选:单线程调度)→ 无锁。
-
请求-响应用 CompletableDeferred 回传。
代码范式
sealed interface Msg {
data class Inc(val n: Int) : Msg
data class Get(val reply: CompletableDeferred<Int>) : Msg
data object Stop : Msg
}
class CounterActor(scope: CoroutineScope) {
private val mailbox = Channel<Msg>(capacity = 64, onBufferOverflow = BufferOverflow.SUSPEND)
private val job = scope.launch(Dispatchers.Default.limitedParallelism(1)) { // 串行
var count = 0
for (m in mailbox) when (m) {
is Msg.Inc -> count += m.n
is Msg.Get -> m.reply.complete(count)
Msg.Stop -> { mailbox.close(); break }
}
}
suspend fun inc(n: Int) = mailbox.send(Msg.Inc(n))
suspend fun get(): Int = CompletableDeferred<Int>().also { mailbox.send(Msg.Get(it)) }.await()
suspend fun stop() { mailbox.send(Msg.Stop); job.join() }
}
调优****
-
邮箱容量要配合策略:状态必达 → SUSPEND;若外界可能“狂点”,可改 DROP_OLDEST 以自保。
-
长任务不要在 actor 内跑(会堵消息),应把耗时踢到外部并以结果消息回投。
易坑****
- 在 actor 内做阻塞/重 IO → 饿死后续消息。
- 忘了 Stop/close → 协程泄漏。
组合技:管线 × 扇入 × 扇出 × Actor
- 现实系统通常是:外界扇入到入口 → Actor 做路由/限流/状态校验 → 按业务 管线多段处理 → 中间重活段 扇出到工作池 → 汇总落地。
- 每个“边界”都明确:容量、策略、线程、错误处理、关停顺序。
调优清单(速查)
- 明确每段业务语义:必达/可丢?只要最新/滑窗?
- 容量从 64 起步,观察队列长度与延迟再调;高频展示段优先 DROP_OLDEST/CONFLATED。
- 重计算移出 Main;IO/CPU 段用 Dispatchers.IO/Default,必要时 limitedParallelism(k)。
- 生产者用 trySend,失败时再 send,既保证吞吐又能背压兜底。
- 统一关停:先停上游→close 中间→等消费干净;紧急停用 cancel()。
- 为“未投递元素”配 onUndeliveredElement 做资源清理。