Kotlin之Channel vs Flow

3,534 阅读3分钟

一 根源

冷 Flow,在有订阅对象时,开始emit事件流。在下图中,foo()的调用,仅仅产生一个引用,注意这个函数未加suspend修饰符。当未开始collect方法调用时,Flow内部没有任何coroutine被激活,此时如果程序在调用collect之前,出现异常等均不会出现资源泄漏;


                                                         图1

热 Channel,一开始就发送事件流,如果订阅对象存在消费延迟的话,导致BackPressure。此时如果在程序过程中未来得及close时,出现异常,会出现资源泄漏。同时如果没有对齐正常接收处理,后台仍然会出现任务继续在操作,比如如果发起网络请求的话,任何会发出请求操作,相关的后续处理等任然会出现激活(特别是类似Activity离开自己的生命周期时)。


                                                   图2

从图二中可以看出,A是主动send出来的,同样B,C直到结束,即所谓的热源。

二 channel怎么设计承压(backPressure)

典型生产者-消费者模型里,如果一方效率低,或一方效率高,即整体效率不平衡,导致一方处理不及时,会堆积处理事务或者导致一方长期处于饥饿状态,整体效率低下。

所以针对channel,增加了cache机制,让生产者生产未能及时处理任务,被压入缓冲区。但涉及到各类业务场景,cache机制有时候并不能满足需求。比如说,消费者处理效率低,有大的缓冲区时,如果生产者一直高效,这个缓冲区还是很快被占满;如果是一个无限的缓冲区,那么有可能会导致缓冲区逐渐快速被占用,导致内存溢出等问题。有设计较为合理的平衡技巧,当缓冲区挤压时,适当的增加消费者等模式,但这个设计需要精细,复杂度很高。

三 Flow自带承压机制

作为cold事件源,如果没有消费者,事件源不会主动emit事件。collect方法以及flow的构造器均为suspend,所以均可以延迟,这样如果某端还没有准备好,此时通过延迟来承压。


                                                             图3

四 Fow的性能

从图1中可以看出,Flow默认是异步,串行化来完成承压机制。但串行机制,会影响整体执行效率(可以看到emit完之后,collect执行工作结束才会触发第二个emit)。内部其实只含一个coroutine在完成工作。为了提高效率,Flow提供了一个操作方法,buffer来完成并行化执行,这样会触发多个coroutine来执行方法,即内部实际上用channel来完成并行化。

foo().buffer().collect {...}

五 与Rx的相关性

Rx中,与channel类似的Subject,但由于channel中如果有capicity,则填满后,如果还没有消费者,那么就会suspend,这一点与ReplaySubject直接覆盖掉老的items做法不一样。比如BroadCastChannel(capicity为0时)等同于Rx里的PublishSubject,其中Capcity为正值时,等同于ReplaySubject,但不会将buffer中的items发送给新来的观察者;ConflatedBroadCastChannel等同于BehaviourSubject;

Rx中,与Flow类似的Flowable,Rx构造时有一个BackPressure的策略。但flow的构造的map,filter操作方法通过flowOn操作方法执行在特定的上线文(比如计算型任务,使用Dispatchers.Default),这样设定并不影响下游的collect方法调用的上下文,即所谓的安全的上线文保护机制。


参考:

1. KotlinConf 2019: Asynchronous Data Streams with Kotlin Flow by Roman Elizarov www.youtube.com/watch?v=tYc…

2. github.com/cnfn/kotlin…