12、Channel的高级用法:BroadcastChannel(已废弃)与SharedFlow替代、Channel的fan-out与fan-in模式、Channel的背压策略

各位同学,今天我们来聊聊Channel的高级玩法。说实话,Channel在协程世界里就像水管工手里的接头——看似简单,但接错了地方,整个系统就崩了。我当年刚接触协程时,就在BroadcastChannel上栽过跟头,后来才发现官方早就给了更好的方案。

一、BroadcastChannel已废弃,为什么?

先说说这个“过气网红”。BroadcastChannel,顾名思义,就是一对多的广播通道。一个生产者发消息,多个消费者都能收到。听起来很美好对吧?

但问题出在哪呢?

  • 线程安全问题:BroadcastChannel内部实现有竞态条件,多线程环境下容易出bug
  • 背压处理不完善:消费者消费速度不一致时,要么丢数据,要么内存暴涨
  • 生命周期耦合:Channel关闭后,所有订阅者都得重新订阅,很麻烦
⚠️ 避坑指南:我曾经在项目里用BroadcastChannel做事件总线,结果线上出现了偶发的消息丢失。查了两天才定位到是BroadcastChannel的offer()方法在背压时静默丢弃数据。后来全部迁移到SharedFlow,问题就解决了。

二、SharedFlow:官方推荐的替代方案

SharedFlow是Flow家族的一员,专门解决一对多场景。它比BroadcastChannel强在哪?我列几个关键点:

特性 BroadcastChannel SharedFlow
背压策略 有限支持 内置多种策略
重放能力 不支持 支持replay缓存
生命周期 与Channel绑定 独立于协程作用域
线程安全 有竞态风险 完全线程安全

说白了,SharedFlow就是BroadcastChannel的“完全体”。你想想看,一个支持重放、支持背压、线程安全的事件总线,谁不爱?

SharedFlow的基本用法

// 创建一个SharedFlow,replay=1表示新订阅者能收到最近一条消息
val sharedFlow = MutableSharedFlow<String>(
    replay = 1,
    extraBufferCapacity = 10,
    onBufferOverflow = BufferOverflow.SUSPEND
)

// 生产者
fun produce() {
    CoroutineScope(Dispatchers.Default).launch {
        repeat(100) {
            sharedFlow.emit("消息$it")
            delay(100)
        }
    }
}

// 消费者1
fun consume1() {
    CoroutineScope(Dispatchers.IO).launch {
        sharedFlow.collect { msg ->
            println("消费者1收到:$msg")
        }
    }
}

// 消费者2
fun consume2() {
    CoroutineScope(Dispatchers.IO).launch {
        sharedFlow.collect { msg ->
            println("消费者2收到:$msg")
        }
    }
}
💡 个人习惯:我一般把replay设为1,这样新订阅者能拿到最新状态。extraBufferCapacity根据业务场景调整,如果消费者处理慢,就设大一点,但别超过100,否则内存扛不住。

三、Channel的fan-out与fan-in模式

这两个模式,说白了就是“分”和“合”的艺术。我在做实时数据管道时经常用到。

Fan-out:一对多分发

一个生产者,多个消费者。每个消费者都能收到全部消息。SharedFlow天然支持fan-out。

// Fan-out示例:一个生产者,两个消费者
val channel = Channel<String>(Channel.UNLIMITED)

fun producer() {
    CoroutineScope(Dispatchers.Default).launch {
        repeat(10) {
            channel.send("数据$it")
            delay(50)
        }
        channel.close()
    }
}

fun consumer(name: String) {
    CoroutineScope(Dispatchers.IO).launch {
        for (msg in channel) {
            println("$name 处理:$msg")
            delay(100) // 模拟处理耗时
        }
    }
}

// 启动两个消费者
consumer("消费者A")
consumer("消费者B")
producer()

注意:这里的Channel是普通的Channel,不是BroadcastChannel。每个消息只能被一个消费者消费。如果想广播,还是用SharedFlow。

Fan-in:多对一汇聚

多个生产者,一个消费者。消息汇聚到一个通道里。这个模式在合并多个数据源时特别有用。

// Fan-in示例:两个生产者,一个消费者
val resultChannel = Channel<String>(Channel.BUFFERED)

fun producer1() {
    CoroutineScope(Dispatchers.Default).launch {
        repeat(5) {
            resultChannel.send("生产者1-数据$it")
            delay(80)
        }
    }
}

fun producer2() {
    CoroutineScope(Dispatchers.Default).launch {
        repeat(5) {
            resultChannel.send("生产者2-数据$it")
            delay(120)
        }
    }
}

fun consumer() {
    CoroutineScope(Dispatchers.IO).launch {
        for (msg in resultChannel) {
            println("消费者收到:$msg")
        }
    }
}

producer1()
producer2()
consumer()

嗯,这里要注意:多个生产者往同一个Channel发消息时,Channel内部会做线程安全处理,不会出现数据错乱。但如果你用Channel.UNLIMITED,小心内存溢出。

四、Channel的背压策略

背压,说白了就是“生产者太快,消费者太慢”的问题。你想想看,如果生产者每秒发1000条消息,消费者每秒只能处理10条,那中间积压的数据怎么办?

Channel提供了几种背压策略:

策略 行为 适用场景
SUSPEND 缓冲区满时,生产者挂起 要求不丢数据,且消费者能跟上
DROP_OLDEST 丢弃最旧的数据 实时性要求高,旧数据无意义
DROP_LATEST 丢弃最新的数据 保证数据连续性,允许丢最新
// 背压策略示例
val channel = Channel<String>(
    capacity = 10,
    onBufferOverflow = BufferOverflow.DROP_OLDEST
)

fun fastProducer() {
    CoroutineScope(Dispatchers.Default).launch {
        repeat(100) {
            channel.trySend("消息$it")
            println("发送:消息$it")
            delay(10)
        }
    }
}

fun slowConsumer() {
    CoroutineScope(Dispatchers.IO).launch {
        for (msg in channel) {
            println("消费:$msg")
            delay(200) // 很慢的消费者
        }
    }
}

fastProducer()
slowConsumer()
🔑 关键点:背压策略的选择取决于业务。如果是日志系统,可以DROP_OLDEST;如果是支付订单,必须SUSPEND保证不丢。我在做股票行情推送时,用的就是DROP_OLDEST,因为旧的价格数据没有意义。

五、知识体系总览

下面这张图,是我梳理的Channel高级用法知识体系。你可以把它当作思维导图来用。

Channel高级用法 BroadcastChannel废弃 • 线程安全问题 • 背压不完善 • 生命周期耦合 SharedFlow替代方案 • replay重放能力 • 多种背压策略 • 完全线程安全 Fan-out / Fan-in • Fan-out:一对多分发 • Fan-in:多对一汇聚 • 数据管道设计 背压策略:SUSPEND / DROP_OLDEST / DROP_LATEST • SUSPEND:生产者挂起,保证不丢数据 • DROP_OLDEST:丢弃最旧数据,适合实时场景 • DROP_LATEST:丢弃最新数据,保证连续性

这张图把今天讲的内容串起来了。从BroadcastChannel的废弃,到SharedFlow的替代,再到fan-out/fan-in模式,最后是背压策略。你想想看,这些知识点其实都围绕一个核心问题:如何高效、安全地在协程间传递数据

💡 实战建议:如果你在写新项目,直接用SharedFlow替代BroadcastChannel。如果是老项目迁移,先把BroadcastChannel替换成SharedFlow,再逐步优化背压策略。我去年重构一个IM项目时就是这么干的,效果很好。

好了,Channel的高级用法就聊到这里。记住一句话:选对工具,事半功倍。SharedFlow是广播场景的首选,背压策略要根据业务场景灵活选择,fan-out和fan-in是数据管道的基石。下次遇到类似需求,你应该知道怎么选了。


公众号:蓝海资料掘金营,微信deep3321