1) Channel 的语义与定位(单播 + 背压原语)
-
单播(unicast) :一个元素只会被一个接收者消费。你可以 fan-in(多生产者)或 fan-out(多消费者“抢”同一条管道),但同一条消息不会被多人同时收到。
-
背压(backpressure) :send/receive 是挂起操作;当队列满或没人接收时,send 会挂起,形成天然背压。
-
**容量(capacity)**决定背压强度:
-
RENDEZVOUS / 0:无缓冲,最强背压(发送与接收一一配对)。
-
BUFFERED(n):有限缓冲,吸收突发;满则背压。
-
CONFLATED:只保最新(丢弃旧的)。
-
UNLIMITED:理论无限队列(极易 OOM,不建议)。
-
onBufferOverflow = DROP_OLDEST / DROP_LATEST:结合 BUFFERED 的丢弃策略。
-
常用 API 速记:
- 发送:send(挂起) / trySend(非挂起,返回 ChannelResult)/ trySendBlocking(阻塞线程;谨慎用于非协程环境)。
- 接收:receive / tryReceive / 推荐:receiveCatching()(能感知关闭)。
- 关闭:close(cause?)(优雅关写端,允许读完缓冲) vs cancel(cause?)(立刻失败并丢弃未读)。
- 状态:isClosedForSend / isClosedForReceive。
2) Actor 是什么?为何适合“单线程串行 + 强背压”
Actor =(邮箱 Channel)+(一个协程的串行事件循环)
- 把可变状态“圈进”一个协程,所有修改都通过“发消息”(send)排队,内部顺序处理 ⇒ 无需锁、天然线程安全。
- 强背压:把邮箱开成 RENDEZVOUS 或小容量,上游“超速”就挂在 send,保证 Actor 不会被洪水淹没。
- 官方 actor{} 构建器在 kotlinx-coroutines 里长期标注 Obsolete/Experimental;更推荐用 Channel + launch 手写 Actor,语义更直观、可控性更好。
3) 两种 Actor 写法
3.1 推荐:Channel + launch 手写
class CounterActor(scope: CoroutineScope) {
// 1) 定义消息协议(强类型!)
sealed interface Msg
data class Inc(val n: Int) : Msg
data class Get(val reply: CompletableDeferred<Int>) : Msg
// 2) 强背压邮箱:0 容量 = RENDEZVOUS
private val mailbox = Channel<Msg>(capacity = 0)
// 3) 串行事件循环(同一协程/线程内操作可变状态)
private val job = scope.launch(Dispatchers.Default) {
var count = 0
for (m in mailbox) when (m) {
is Inc -> count += m.n
is Get -> m.reply.complete(count)
}
}
// 4) 对外暴露“写端”(单播)
val inbox: SendChannel<Msg> get() = mailbox
suspend fun closeAndJoin() { mailbox.close(); job.join() }
}
要点:0 容量让 send 与 receive 对接 → 最强背压;Actor 内部的状态更新严格串行。
3.2(了解即可)actor{}构建器
@OptIn(ObsoleteCoroutinesApi::class)
fun CoroutineScope.counterActor() = actor<CounterMsg>(capacity = 0) {
var count = 0
for (m in channel) when (m) { /* … */ }
}
// 返回的是 SendChannel<CounterMsg>,内部通过 channel 接收
受限于过时/实验标记与可读性,生产更建议用 手写 版本。
4) 典型模板(照抄能用)
4.1 强背压流水线(RENDEZVOUS,避免堆积)
val ch = Channel<Task>(capacity = 0) // 一手交钱一手交货
val worker = scope.launch {
for (t in ch) doWork(t) // 严格串行
}
// 生产处:没有空闲消费者就挂起
scope.launch { tasks.forEach { ch.send(it) }; ch.close() }
适用:GPU/IO 受限、设备资源稀缺(解码、渲染、磁盘),“宁慢勿堆”。
4.2 限流 + 抗抖(小缓冲 + 丢弃旧帧)
val updates = Channel<Update>(
capacity = 64,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
val actor = scope.launch(Dispatchers.Default) {
for (u in updates) applyUpdate(u) // 只保住“最新的 64 条”
}
适用:UI 状态/遥测上报,只关心最新,不需处理全部历史。
4.3 “只要最新”工作(取消旧任务 → mapLatest 的手写 Actor 版)
sealed interface Msg { data class Start(val job: suspend () -> Unit) : Msg }
val mbox = Channel<Msg>(capacity = 64)
scope.launch {
var running: Job? = null
for (m in mbox) when (m) {
is Msg.Start -> {
running?.cancel()
running = launch { m.job() } // 最新的生效
}
}
}
4.4 请求-应答(Ask 模式)
sealed interface Msg
data class Put(val k: String, val v: Int) : Msg
data class Get(val k: String, val reply: CompletableDeferred<Int?>) : Msg
val mbox = Channel<Msg>(capacity = 0)
val kvActor = scope.launch {
val map = HashMap<String, Int>()
for (m in mbox) when (m) {
is Put -> map[m.k] = m.v
is Get -> m.reply.complete(map[m.k])
}
}
// 调用方
suspend fun get(k: String): Int? {
val reply = CompletableDeferred<Int?>()
mbox.send(Get(k, reply))
return reply.await()
}
要点:通过 CompletableDeferred 往回“回信”,仍保持单播与串行。
5) 关闭与清理(谁关、何时关、怎么关)
- 谁生产谁 close() :关闭写端,允许消费者把缓冲读完后自然退出 for (e in ch)。
- 消费者不想再读:cancel(cause),会立刻让挂起中的 send/receive 失败并丢弃未读。
- Actor 收尾:常用 close(); job.join() 或 cancelAndJoin();在 finally 中用 withContext(NonCancellable) 做资源释放。
- 错误传播:Actor 内未捕获异常会取消其 Job 并使 channel 失败;上游 send 会收到异常(或 trySend 失败)。在 Actor 内部 try/catch,按需上报并决定是否 close()。
6) Channel/Actor vs Flow/SharedFlow(怎么选)
- Channel/Actor:点对点单播、需要明确背压/容量控制、需要命令式消息协议(如状态机/命令处理)。
- SharedFlow/StateFlow:广播、多订阅者、无背压(SharedFlow 只提供缓冲 + 重放而非挂起背压),状态/事件分发优选。
- 互转:ReceiveChannel.consumeAsFlow();flow.produceIn(scope)。
7) 性能与调度小抄
- 容量小即强背压,避免无界内存增长;多数业务用 0/1/64 足矣。
- CPU 密集型 Actor 绑到 Dispatchers.Default;UI Actor 绑 Main/Main.immediate。
- 不要在 Actor 内阻塞线程(sleep/IO 阻塞),用挂起 API;确需阻塞改用 withContext(IO) 包起来。
- 高并发场景可建固定数量 worker(多个消费者 fan-out 同一 Channel);需要顺序一致就单 Actor。
8) 高频坑与排错
- 双向等待死锁:两个 Actor 用 RENDEZVOUS 互相 send → 都在等对方 receive。解决:引入缓冲或一侧改为 trySend + 重试/协商。
- 不小心广播:想“一条消息给所有人”却用 Channel,导致只有一人收到。需要广播请用 SharedFlow 或多播设计。
- UNLIMITED 内存暴涨:见过把 Bitmap 往 UNLIMITED 里塞到 OOM。一定用小容量+背压/丢弃策略。
- 多处 close() :close 只能一次;重复 send 到已关闭通道会失败。明确“关闭权责”(通常生产者)。
- 消耗不干净:for (e in ch) 之外别忘了在 finally 里清理资源;使用 receiveCatching() 感知关闭。
- 混用阻塞发送:trySendBlocking 在 UI 线程会卡死;仅在非协程环境或确知可以阻塞时用。
- 吞异常:Actor 内 catch 了但不告知外部,通道持续背压。应上报/打点,并在致命错误后 close(cause)。
9) 设计指北(三步走)
-
写出消息协议(sealed class Msg)——把“能做的事”类型化。
-
选邮箱容量策略:
- 实时一致性/不能堆积 → 0(强背压);
- 可吸突发 → 小 BUFFERED(n);
- 只要最新 → BUFFERED + DROP_OLDEST 或 CONFLATED。
-
确定并发度:
- 必须顺序 → 单 Actor;
- 可并发处理 → 固定 worker 数量 repeat(k) { launch { for (t in ch) … } }。
一句话总结
-
Channel = 单播消息队列 + 背压;Actor = Channel + 串行处理的协程。
-
强背压选 RENDEZVOUS/小缓冲;只要最新用 DROP_OLDEST/CONFLATED。
-
谁生产谁 close;消费者不再读用 cancel。
-
想广播请用 SharedFlow;需要可变状态串行执行业务逻辑,Actor 最省心。