生成者消费模式,在设计中也是一种频繁的应用,掌握它便能轻松应对各种场景

一、前言
生产消费者模式在Andoird 里面应用还相当广泛,它带来的好处大致可以分为:
- 解耦:能彻底解耦生产模块和消费模块,生产者和消费者之间不直接进行交互,而是通过一个缓冲区(如队列或缓冲区)来进行交互。这种方式减少了生产者和消费者之间的直接依赖关系,使得系统的各个部分更加独立,易于维护和扩展
- 同时,它也能平衡生产消费之间的速度。在多线程环境中,生产者和消费者的处理速度往往是不一致的。生产者-消费者模式通过缓冲区来平衡这种速度差异,生产者只需将数据放入缓冲区,而消费者从缓冲区取出数据处理。这样,生产者不需要等待消费者处理完数据再继续生产,反之亦然,从而提高了系统的整体效率
- 提高系统稳定性和效率:通过缓冲区的使用,系统可以在生产者速度过快或消费者速度过慢时进行调节,避免了直接的生产者-消费者间的等待,减少了系统负载,提高了程序的稳定性和效率
- 应用广泛 :生产者-消费者模式在许多系统中都有应用:
Android系统中的 :Handler,Looper, message,MessageQueue
它们的运行模式就是一种生成消费者模式。
同样:在Android直播应用中
:我们把每一帧的数据编码好,准备成传输数据包:RTMPPackage
加入到缓冲队列里面,这个过程就是数据生成阶段,另一边我们不停地从缓冲队列里面取出 传输数据包:RTMPPackage
,推送给服务端,这个过程就是消费阶段。
-
这里需要注意的,生产消费者模式并不是23种设计模式中的。
-
本文主要介绍:
Kotlin+协程+FLow+Channel,实现生产消费者模式几种案例
二、案例一,传统式写法:
- 生产者向缓冲区产生数据,当缓冲区超过设置最大数时候,让其等待,等待消费者处理完了,再加入进去。
- 当消费者发现缓冲区为空时候,等待,知道有可以消费的数据时候为止。
- 关键点:使用while循环检查条件避免虚假唤醒,notifyAll确保唤醒正确线程
kotlin
val buffer = LinkedList<Int>()
val MAX_SIZE = 5
val lock = Object()
// 生产者
fun produce(item: Int) {
synchronized(lock) {
while (buffer.size >= MAX_SIZE) {
lock.wait() // 缓冲区满时等待
}
buffer.add(item)
lock.notifyAll() // 必须使用notifyAll避免死锁
}
}
// 消费者
fun consume(): Int {
synchronized(lock) {
while (buffer.isEmpty()) {
lock.wait() // 缓冲区空时等待
}
val item = buffer.removeFirst()
lock.notifyAll()
return item
}
}
// 测试代码
fun main() {
repeat(3) { i ->
Thread { produce(i) }.start() // 启动3个生产者
}
repeat(2) {
Thread { println(consume()) }.start() // 启动2个消费者
}
从上面我们可以看出,最经典的,是完全自由的,可完全自定义的,涉及到缓存,最大值,锁相关知识,以及如果 异步
也需要自己来实现管理处理。这样写起来需要基本功深才行。
三、案例二、协程+Channel方式实现
下面示例:
我们创建了容量为5的缓冲通道,
生产者每100ms发送数据,
消费者每200ms处理数据,
自动实现流量控制。
当缓冲区满时send挂起,
空时receive挂起
同时,由于我们协程中,我们可以通过直接通过协程中的 Dispatchers
来直接切换线程,让其异步中实现生产和消费。
scss
fun main() = runBlocking {
// 创建容量为5的缓冲通道
val channel = Channel<Int>(capacity = 5)
// 生产者协程
val producer = launch {
repeat(10) { i ->
delay(100) // 模拟生产耗时
channel.send(i)
println("生产: $i (缓冲区剩余: ${channel.capacity - channel.size})")
}
channel.close() // 生产完成后关闭通道
}
// 消费者协程
val consumer = launch {
channel.consumeEach { item ->
delay(200) // 模拟消费耗时
println("消费: $item")
}
}
// 等待生产和消费完成
joinAll(producer, consumer)
}
四、案例三、协程结合Flow方式实现
如下面代码所示:
我们通过flow构建生产者流,buffer控制背压,onEach处理消费逻辑,flowOn实现线程切换 涉及到的核心点:
-
背压策略选择
buffer()
:缓冲指定数量元素,默认溢出策略为挂起生产者
conflate()
:仅保留最新元素,适合实时状态更新场景
collectLatest()
:新元素到达时取消当前消费任务 -
线程调度控制
flowOn(Dispatchers.IO)
:指定上下游执行线程池生产者与消费者默认共享协程上下文,需显式分离避免阻塞
-
异常处理增强
可通过Flow的.catch统一处理生产阶段和消费阶段的异常。
scss
fun producer(): Flow<Int> = flow {
repeat(10) { i ->
delay(100) // 模拟生产耗时
emit(i)
println("生产: $i")
}
}.catch{
e -> println("生产异常: $e")
}
.flowOn(Dispatchers.IO) // 指定生产者协程上下文
fun main() = runBlocking {
producer()
.buffer(5) // 设置缓冲区容量
.onEach { item ->
delay(200) // 模拟消费耗时
println("消费: $item")
}.catch{
e -> println("生产异常: $e")
}.flowOn(Dispatchers.IO) // 指定消费者协程上下文
.collect()
}
从上面我们也可以看出:
Flow与Channel方案对比
特性 | Flow方案 | Channel方案 |
---|---|---|
数据生成方式 | 冷流(按需生产) | 热流(独立于消费) |
背压处理 | 通过操作符(buffer/conflate)控制 | 依赖通道容量设置 |
异常处理 | 统一处理 | 需要手动分别处理 |
适用场景 | 纯数据流处理 | 需要双向通信的场景 |
五、总结
本文重点介绍了,传统生产消费者模式和 Kotlin+协程+FLow+Channel实现消费者模式
- 传统的完全自定义,涉及到相关内容较多,需要基础扎实
- 基于协程和Flow或者协程+Channel来实现,可以很简单的背压处理,切换线程处理。使用起来更加方便。