Kotlin 协程 - 热流 Channel、SharedFlow、StateFlow

本文介绍Kotlin协程中的热流,包括Channel、SharedFlow和StateFlow。Channel作为协程间通信的并发安全缓冲队列,有多种创建和操作方式。SharedFlow和StateFlow用于广播,StateFlow是SharedFlow的特定配置版本。还阐述了组播与广播、事件与状态的区别及选择。

一、概念

1.1 通信模式

根据接收端节点数量以及接收方式,可分为三类。

单播 Unicast一对一,对单个订阅者发送。
组播 Multicast分配性:一对多,依次对单个订阅者发送。多个订阅者互斥,收到的值不是同一个。同步性:元素只能被一个接收端消费。
广播 BroadCast共享性:一对多,同时对全体订阅者发送。多个订阅者共享,接收到的值是同一个。并发性:一次发送多处消费。

1.2 扇入扇出

扇入 Fan-in指直接调用该模块的上级模块的个数(多个发送端),即多个协程可能会向同一个Channel发送值。扇入大表示模块的复用程序高。
扇出 Fan-out指该模块直接调用的下级模块的个数(多个接收端),即多个协程可能会从同一个Channel中接收值。扇出大表示模块的复杂度高。

二、协程间通信

        出现在Flow之前,现在退居幕后职责单一,仅作为协程间通信的并发安全的缓冲队列而存在。

        SendChannel在创建时定义了消费方式外界只能往里发送值,ReceiveChannel在创建时定义了生产方式外界只能从中获取值,Channel继承了它俩既能发送也能接收,根据实际需求暴露不同类型收窄功能。

  • 非阻塞:类似于 Java 中的 BlockingQueue 队列,不同的是 put() 和 take() 读取写入数据是阻塞的,而 Channel 中的 send() 和 receive() 是挂起的。
  • 同步性:每个值只能被众多订阅者中的一个消费。
  • 并发安全:没有检测到 receive() 的话 send() 就会挂起不会发送值(默认模式 RENDEZVOUS,没有缓冲区的Channel是同步的)。
  • 公平性:在多个协程中发送或接收(多线程竞争)遵循先进先出(FIFO即队列)。

SendChannel

生产者通道

send()

public suspend fun send(element: E)

将元素发送到通道,缓冲区已满时将被挂起,直到有旧元素被消费腾出空间。

trySend()

public fun trySend(element: E): ChannelResult<Unit>

如果不违反其容量限制,则立即将指定元素添加到此通道,并返回成功结果。否则返回失败或关闭的结果。

close()

public fun close(cause: Throwable? = null): Boolean

调用后表示关闭发送功能,此时 isClosedForSend() 会返回true,继续发送元素会报错ClosedSendChannelException。缓冲区里的元素可以继续被消费,等所有元素被消费后 isClosedForReceive() 会返回true,for循环会自动结束,继续消费会报错ClosedReceiveChannelException。具有原子性。

isClosedForSend()

public val isClosedForSend: Boolean

判断通道是否关闭了发送功能,若为true,调用 send() 继续发送元素会报错ClosedSendChannelException。

ReceiveChannel

消费者通道

receive()

public suspend fun receive(): E

从通道中消费一个元素并移除,通道中没有元素时将被挂起,直到有新元素被发送进来。

tryReceive()

public fun tryReceive(): ChannelResult<E>

当通道中有值时就从中消费,并返回成功结果。否则返回失败或关闭的结果。

receiveCatching()

public suspend fun receiveCatching(): ChannelResult<E>

如果此通道不为空,则从中检索并删除元素,返回成功结果;如果通道为空,则返回失败结果;如果通道关闭,则返回关闭的原因。

isEmpty()

public val isEmpty: Boolean

若通道中没有元素且接收未被关闭则返回true。

isClosedForReceive()

public val isClosedForReceive: Boolean

判断通道是否关闭了消费功能,若为ttrue,调用 receive() 继续消费元素会报错ClosedReceiveChannelException。

cancel()

public fun cancel(cause: CancellationException? = null)

以可选原因直接关闭通道,缓存中未消费的元素会被丢弃,可在通过构造方式创建通道时指定 onUndeliveredElement() 进行处理。

iterator()

public operator fun iterator(): ChannelIterator<E>

返回通道的迭代器。

  • send() 和 receive():挂起函数。当接收时,Channel中没有元素,协程将被挂起直到元素可用。当发送时,Channel容量达到阈值,协程将被挂起直到容量可用。
  • trySend() 和 tryReceive():普通函数。从非挂起函数中发送或接收元素,操作是即时的并返回 ChannelResult 对象,包含了有关操作成功或失败的信息。
  • close() 和 cancel():当创建的是 Channel 类型的时候,两个都可以用来关闭,close() 会消费完缓冲区中的元素,cancel() 会直接关闭并丢未消费的元素。通道使用完一定要关闭,否则代码块不会停止,造成协程阻塞和内存泄漏。
fun main() = runBlocking {
    private val _channel = Channel<String>()
    val receiveChannel: ReceiveChannel<String> = _channel
    val sendChannel: SendChannel<String> = _channel
    
    _channel.send("Channel发送")
    sendChannel.send("SendChannel发送")
    delay(1000)
    _channel.consumeEach { println("Channel接收:$it") }
    receiveChannel.consumeEach { println("ReceiveChannel接收:$it") }
}

2.1 创建

produce() 和 actor() 被定义成协程构建器(因此只能在协程环境中调用,会在异常、完成、取消时自动关闭),同 launch、async 一样作为 CoroutineScope 的扩展函数。

2.1.1 actor() 创建 SendChannel

创建时在协程构建器 actor() 的 Lambda 中定义了数据的消费方式,返回一个生产者通道 SendChannel,其它协程通过该对象往里发送数据。

public fun <E> CoroutineScope.actor(
    context: CoroutineContext = EmptyCoroutineContext,
    capacity: Int = 0,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    onCompletion: CompletionHandler? = null,
    block: suspend ActorScope<E>.() -> Unit
): SendChannel<E>
fun main() = runBlocking {
    //创建生产者通道
    val send: SendChannel<Int> = actor {
        for (i in channel) println(i)    //定义了消费方式
    }
    //其它协程拿来发送数据
    launch {
        (1..3).forEach { sendChannel.send(it) }
    }
}

2.1.2 produce() 创建 ReceiveChannel

创建时在协程构建器 produce() 的 Lambda 中定义了数据的生产方式,返回一个消费者通道 ReceiveChannel,其它协程通过该对象从中取出数据。

public fun <E> CoroutineScope.produce(
    context: CoroutineContext = EmptyCoroutineContext,
    capacity: Int = 0,
    @BuilderInference block: suspend ProducerScope<E>.() -> Unit
): ReceiveChannel<E>

fun main() = runBlocking {
    //创建消费者通道
    val receiveChannel: ReceiveChannel<Int> = produce {
        (1..3).forEach { send(it) }    //定义了生产方式
    }
    //其它协程拿来接收数据
    launch {
        for (i in receiveChannel) println(i)
    }
}

2.1.3 构造创建 Channel

public fun <E> Channel(
    capacity: Int = RENDEZVOUS,        //容量
    onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND,        //容量溢出后策略
    onUndeliveredElement: ((E) -> Unit)? = null        //异常回调
): Channel<E>

capacity

缓冲区容量

没有缓冲区的Channel是同步的。当缓冲区为空时(缓存中没有元素),receive会被挂起。

RENDEZVOUS(默认):或指定为0。缓冲区大小为0且为SUSPEND策略,每次一个值,receive和send同步交替,另一方没准备好自己就会挂起。(并发安全)

CONFLATED:或指定为1。只有1个值,新值覆盖旧值,send永不挂起。此时溢出策略只能设为SUSPEND。

BUFFERED:或指定为-2(默认为64个值)或指为>1的具体值。缓冲区满了后根据溢出策略决定send是否被挂起(挂起直到消费后腾出空间)。

UNLIMITED:无限容量(Int.MAX_VALUE)。send永不挂起,能一直往里发送数据。实际开发一般不用,容易内存溢出。

onBufferOverflow

缓冲区满后溢出策略

当容量 >= 0 或容量 == Channel.BUFFERED 时才会触发。

BufferOverflow.SUSPEND满了后send会挂起。

BufferOverflow.DROP_OLDEST丢弃最旧的值,发送新值。

BufferOverflow.DROP_LATEST丢弃新值。

onUndeliveredElement

异常回调

元素没有被消费时回调,丢弃的元素会在这里收到。通常用来关闭由Channel发送的资源(值)。
val rendezvousChannel = Channel<String>()    //约会类型
val bufferedChannel = Channel<String>(10)    //指定缓存大小类型
val conflatedChannel = Channel<String>(Channel.CONFLATED)    //混合类型
val unlimitedChannel = Channel<String>(Channel.UNLIMITED)    //无限缓存大小类型
fun main() = runBlocking {
    val channel = Channel<Int>(capacity = Channel.CONFLATED) {
        println("onUndeliveredElement: $it")
    }
    launch {
        (1..3).forEach {
            println("send: $it")
            channel.send(it)
        }
        channel.close()    //不要忘记关闭
    }
    launch {
        for (i in channel) {
            println("receive: $i")
        }
    }
    println("结束!")
}

打印:
结束!
send: 1
send: 2
onUndeliveredElement: 1
send: 3
onUndeliveredElement: 2
receive: 3

2.2 遍历元素

2.2.1 Iterate 迭代器

fun main(): Unit = runBlocking {
    val channel = Channel<String>(Channel.UNLIMITED)
    repeat(5) { channel.send("$it") }
    val iterator = channel.iterator()
    while (iterator.hasNext()) {
        println("【iterator】${iterator.next()}")
        delay(1000)
    }
    //5个元素打印完后程序没结束,Channel不会关闭,后面代码执行不到
    println("这行执行不到")
}

2.2.2 for 循环

fun main():Unit = runBlocking {
    val channel = Channel<String>(Channel.UNLIMITED)
    repeat(5) { channel.send("$it") }
    for (i in channel) {
        println("【for】$i,")
        delay(1000)
    }
    //5个元素打印完后程序没结束,Channel不会关闭,后面代码执行不到
    println("这行执行不到")
}

2.2.3 扩展函数

会在代码块执行完后确保关闭通道,但无法保证在代码块执行完后不会有新元素进入,新元素会被丢弃,可在通过构造方式创建通道时指定 onUndeliveredElement() 进行处理。

consume()

public inline fun <E, R> ReceiveChannel<E>.consume(block: ReceiveChannel<E>.() -> R): R

执行完后关闭通道。

consumeEach()

public suspend inline fun <E> ReceiveChannel<E>.consumeEach(action: (E) -> Unit): Unit

将 action 应用于每一个元素,执行完后会关闭通道。

//只消费第一个元素并关闭通道
suspend fun <E> ReceiveChannel<E>.consumeFirst(): E = consume { return receive() }

2.2.4 转换成 Flow

底层通过 ChannelFlow 实现。Channel 转成 Flow 是为了使用那些方便的操作符,同时 Flow 的很多功能扩展底层由 Channel 实现(如flowOn、buffer)。

consumeAsFlow()

public fun <T> ReceiveChannel<T>.consumeAsFlow(): Flow<T> = ChannelAsFlow(this, consume = true)

只能被收集一次(只能有一个消费者),多次收集抛异常 IllegalStateException。

receiveAsFlow()

public fun <T> ReceiveChannel<T>.receiveAsFlow(): Flow<T> = ChannelAsFlow(this, consume = false)

采用扇出模式(可以有多个消费者),一个元素只能被消费一次,该元素的消费者不确定是哪个。

三、广播

BroadcastChannel 在协程1.4版本已被废弃,取而代之的是 SharedFlow(StateFlow是它的特定配置版本)。

3.1 SharedFlow

SharedFlow只能订阅(消费),FlowCollector只能发送(生产),MutableSharedFlow继承了它俩既能发送也能订阅,根据实际需求暴露不同类型收窄功能。接收会一直监听,通过取消协程来关闭它的收集。

SharedFlow

public interface SharedFlow<out T> : Flow<T> {

    //缓存的回放元素的快照
    public val replayCache: List<T>

    //收集元素

    override suspend fun collect(collector: FlowCollector<T>): Nothing
}

FlowCollectorpublic fun interface FlowCollector<in T> {
    public suspend fun emit(value: T)        //发送元素
}
MutableSharedFlowpublic interface MutableSharedFlow<T> : SharedFlow<T>, FlowCollector<T> {
    // 发射元素(注意这是个挂起函数)
    override suspend fun emit(value: T)
    // 发射元素(注意这是个普通函数,如果缓存溢出策略是 SUSPEND,溢出时就不会挂起了而是直接返回 false)
    public fun tryEmit(value: T): Boolean
    // 活跃订阅者数量,将它设为0生产元素就会停止用来释放资源。
    public val subscriptionCount: StateFlow<Int>
    //清空当前回放里的历史记录
    public fun resetReplayCache()
}
fun main() = runBlocking {
    //利用多态暴露不同父类限制功能给外部使用
    private val _mutableSharedFlow = MutableSharedFlow<String>()
    val sharedFlow: SharedFlow<String> = _mutableSharedFlow    //或调用 asSharedFlow()
    val flowCollector: FlowCollector<String> = _mutableSharedFlow

    launch { _mutableSharedFlow.collect { println("mutableSharedFlow接收:$it") } }
    launch { sharedFlow.collect { println("mutableSharedFlow接收:$it") } }
    delay(1000)
    mutableSharedFlow.emit("mutableSharedFlow发送")
    flowCollector.emit("flowCollector发送")
}

3.1.1 通过构造创建

public fun <T> MutableSharedFlow(
    replay: Int = 0,        //回放,观察者订阅后能得到几个订阅前已经发出过的旧元素。
    extraBufferCapacity: Int = 0,        //额外缓存容量,加上replay才是总共的缓存数量。
    onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND        //缓存溢出策略。
): MutableSharedFlow<T>

onBufferOverflow

缓存溢出策略

1.只有存在订阅者时才会触发缓存溢出策略,否则直接丢弃。(例如两个launch虽然会并行执行,若下方的消费launch在上方的生产launch后执行,先发送的值都会被丢弃接收不到,直到消费launch执行了才开始接收后面的值。这是和 Channel 最大的区别,没有消费者 Channel 不会丢)

2.只有在回放或缓存容量>0时,才支持两种丢弃模式。

BufferOverflow.SUSPEND:挂起。

BufferOverflow.DROP_OLDEST:丢弃最旧的。

BufferOverflow.DROP_LATEST:丢弃最新的。

suspend fun method(): Unit = coroutineScope {   //让3个launch同时进行
    val shared = MutableSharedFlow<Int>(3)    //回放3个数据
    launch {
        for(i in 1..5){
            shared.emit(i)
            println("emmit:$i")
            //如果这里不延迟,虽然launch是并行执行,发送是很快的这里才5个值
            //相当于是没有订阅者的,5个值都会被丢弃
            //除非把下面的订阅launch写在这个launch上面
            delay(1000) //1秒更新一个值
        }
    }
    //订阅者甲
    launch { shared.collect{ println("甲:$it") } }
    //订阅者乙5秒后再订阅,也就是值全都更新完了再订阅
    delay(5000)
    launch { shared.collect{ println("乙:$it") } }
}

打印:
emmit:1
甲:1
emmit:2
甲:2
emmit:3
甲:3
emmit:4
甲:4
emmit:5
甲:5
乙:3    //回放了3个已更新过的数据
乙:4
乙:5

3.1.2 转换 Flow → SharedFlow

public fun <T> Flow<T>.shareIn(
    scope: CoroutineScope,        //数据共享时所在的协程作用域
    started: SharingStarted,        //启动策略
    replay: Int = 0        //回放,新订阅时得到几个之前已经发射过的旧值。
): SharedFlow<T>

started

启动策略

SharingStarted.Eagerly立即发送数据(直到scope结束)。
SharingStarted.Lazily在首个订阅者观察时才开始发送数据(当订阅者都没了还是活跃的,直到scope结束)。这保证了第一个订阅者能获得所有值,后续订阅者获得最新replay数量的值。
SharingStarted.WhileSubscribed

在首个订阅者观察时才开始发送数据,直到最后一个订阅者消失时停止,当又有新订阅者时会再次启动,避免引起资源浪费(例如一直从数据库、传感器中读取数据)。提供了两个配置:

  • stopTimeoutMillis:超时时间。最后一个订阅者消失后,数据流继续活跃多久用于等待新订阅者,默认值0表示立刻停止。避免订阅者都消失后就马上关闭数据流(例如不想UI有那么几秒不再监听就停止)。
  • replayExpirationMillis:回放过期时间。数据流停止后,保留回放数据的超时时间,默认值 Long.MAX_VALUE 表示永久保存。

3.2 StateFlow

StateFlow继承自SharedFlow是一种特殊配置,相当于MutableSharedFlow(1,0, BufferOverflow.DROP_OLDEST),可以使用value属性来访问值,可以当作是用来取代LiveData。

  • 必须传入默认值:Null安全。
  • 回放个数1+额外缓冲区大小0:只持有1个值。
  • 缓存策略DROP_OLDEST:只持有最新值(新值会替换旧值)。
  • 数据防抖:即仅在新值内容发生变化才会消费。
StateFlowpublic interface StateFlow<out T> : SharedFlow<T> {
    // 当前值
    public val value: T
}
MutableStateFlowpublic interface MutableStateFlow<T> : StateFlow<T>, MutableSharedFlow<T> {
    // 当前值
    public override var value: T
    // 比较并设置(通过 equals 对比,如果值发生真实变化返回 true)
    public fun compareAndSet(expect: T, update: T): Boolean
}

3.2.1 通过构造创建

public fun <T> MutableStateFlow(value: T): MutableStateFlow<T> = StateFlowImpl(value ?: NULL)

形参value是默认值。

val state = MutableStateFlow(0) //默认值会被覆盖
launch {
    for(i in 1..5){
        state.emit(i)
        println("emit:$i")
        delay(1000) //1秒更新一个值
    }
}
launch {
    delay(2000) //2秒后开始订阅
    state.collect{ println("collect:$it") }
}

打印:
emit:1
emit:2
collect:2 //2秒后开始订阅,不会收到之前已更新的数据
emit:3
collect:3
emit:4
collect:4
emit:5
collect:5

3.2.2 转换 Flow → StateFlow

public fun <T> Flow<T>.stateIn(
    scope: CoroutineScope,        //数据共享时所在的协程作用域
    started: SharingStarted,        //启动策略
    initialValue: T        //默认值
): StateFlow<T>

public suspend fun <T> Flow<T>.stateIn(scope: CoroutineScope): StateFlow<T> {
    val config = configureSharing(1)
    val result = CompletableDeferred<StateFlow<T>>()
    scope.launchSharingDeferred(config.context, config.upstream, result)
    return result.await()
}

挂起函数版本,不用指定默认值,会挂起直到产出第一个值。

四、区别及选择

4.1 通信(Channel) or 广播(SharedFlow)

ChannelSharedFlow
必须性:事件不会被丢弃且必须执行。时效性:过期的事件没有意义且不应该被延迟消费(就像广播电台,不管有没有人收听都在播放内容,当你开始收听的时候只能听到后续新内容,之前的内容就是错过)。
分配性:挨个对订阅者发送,多个订阅者互斥,收到的值不是同一个。共享性:同时对全体订阅者发送,多个订阅者共享,接收到的值是同一个。
同步性:事件只能消费一次。并发性:事件会被多处消费。

4.2 事件(Event) or 状态(State)

事件按顺序都要执行到,状态只关心最新值。

SharedFlowStateFlow
类型回放和额外缓存默认为0,无订阅者直接丢弃数据,符合时效性事件特点。仅持有单个且最新的数据。
初始值无(事件发生后才处理,不需要默认值)有(UI组件应当一直有一个值来表明其状态)
回放

默认0可配置(新的订阅者不重复处理已发生过的事情,或者需要知道之前发生过的事件)

1(新的订阅者也应该知道当前状态,若用作事件处理会出现粘性事件)

额外缓冲区默认0可配置

0(UI组件只显示最新的值)

缓存模式默认SUSPEND(等待消费)DROP_OLDEST(UI组件只显示最新的值)
发送重复的值会消费(事件都应该被处理)。

不消费(防抖,无变化不用处理)。

收集方式只能调用方法 collect() 在协程中收集。既可以通过调用属性 value 随处收集,也可以调用方法 collect() 在协程中收集。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值