阅读 753

深入理解Kotlin协程(三)

热流

来看一下简单的 生产者消费者 模型:

fun main(){

    GlobalScope.launch {
        val channel= Channel<Int>()
        val producer=GlobalScope.launch {
            var i=0
            while (true){
                delay(1000)
                channel.send(i++)
            }
        }

        val consumer=GlobalScope.launch {
            while (true){
                val element=channel.receive()
                println(element)
            }
        }
        producer.join()
        consumer.join()
    }

    Thread.sleep(10000)

}

复制代码

生产者 每隔一秒 发送一个值,消费者 一直读取这个值 并且打印 很简单的例子 但是背后的细节 极多,这里一定要好好掌握,热流绝非网上大部分人云亦云的文章 所说的那么简单,搞不清楚 很容易 使用的时候埋坑

先来看一下ide 的表现:

image.png

红框标出来的很明显, 也就是说 channel的send和receive 都是 挂起函数 ,这是什么意思?

从上面的例子可以看见出来,因为发送端是延迟1s才发数据的,而 接受端 是一直在读数据,那么这里

发送端肯定会比 接收段慢,也就是说 当程序开始运行的时候,接收端 一直收不到数据 所以receivee函数 就 一直挂起在那里了,直到有新元素到达。

那么send函数 也是个挂起函数,send会不会也出现 一直挂起等待的情况呢?

send 会挂起吗?

还是刚才那个例子 我们稍微改一下:

GlobalScope.launch {
    val channel= Channel<Int>()
    val producer=GlobalScope.launch {
        var i=0
        while (true){
            delay(1000)
            i++
            println("send $i")
            channel.send(i)
        }
    }

    val consumer=GlobalScope.launch {
        while (true){
            delay(2000)
            val element=channel.receive()
            println("receive $element")
        }
    }
复制代码

这一次 我们让 receive 比 send 慢一点, 看看send 会不会被挂起?

image.png

从图中很明显的能看出来是吧 send函数 总是要等到 receive了以后 自己才能执行。

问题出在哪呢》?看下我们的channel定义就可以了

image.png

其实这个默认值 RENDEZVOUS 意思就是 这个channel的缓冲区是0,讲白了就是不见不散的机制 只要你没receive 那我就不会send,一定是要我之前send出去的数据 你receive了以后 我才会发下一个

大家可以尝试一下 试试不同的参数 会有什么不同的效果

val channel= Channel<Int>(capacity = Channel.UNLIMITED)
复制代码

接收者的另外几种写法

image.png

image.png

这两种写法 可能更加直观一些,唯一要注意的时候 iterator的写法 她会在hasNext 那边就会挂起了 同样的 第二种写法 在in 语句的时候 就会挂起了

channnel的关闭

来看一个例子:

GlobalScope.launch {
    val channel= Channel<Int>(3)
    val producer=GlobalScope.launch {
        List(3){
            channel.send(it)
            println("send $it")
        }
        channel.close()
        println("close channel  send: ${channel.isClosedForSend}  receive:${channel.isClosedForReceive}")
    }


    val consumer=GlobalScope.launch {
        for (element in channel){
            println("receive $element")
            delay(1000)
            println("close channel  send: ${channel.isClosedForSend}  receive:${channel.isClosedForReceive}")
        }
    }
    producer.join()
    consumer.join()
}
复制代码

我们创建了一个缓存区为3个大小的 channel,然后生产者 发送3个数据以后,直接调用了channel的close 方法

消费者 每隔1s 收一条数据

image.png

看下执行结果可以看出来,receive 端 一定是等到全部接收完毕以后 整个channel才会真正的关闭的。

对于channel的使用, 了解到这个程度 应该说就足够了,还有一些 还处于实验性质的api 这里就不再一一介绍了

大家只要谨记:channel的使用 一定要在合适的时候 主动调用close,否则接收端 大概率一直挂起,虽然不会内存泄漏 但是总归是会占用一定的系统资源,虽然这个资源小到可以忽略不计,对于channel的close,我们只要谨记从发送端开始主导close就可以了,原因?

原因当然是:只有发才会有收吗,这是符合人类逻辑的 毕竟大家都是等人通知要给你送生日礼物 你才会去收的吗

此外,channel还有一个特点是 发送端并不依赖接收端,这与我们下面马上要介绍的flow 冷流是截然不同的

flow的初体验

fun main()= runBlocking{
    val intFlow=flow{
        (1..3).forEach {
            println("emit:"+Thread.currentThread().name)
            emit(it)
            delay(100)
        }
    }
    val coroutineDispatcher=Executors.newFixedThreadPool(3).asCoroutineDispatcher()

    GlobalScope.launch(coroutineDispatcher) {
        intFlow.flowOn(Dispatchers.IO).collect {
            println("collet:"+Thread.currentThread().name)
            println(it)
        }
    }

    Thread.sleep(300000)
}
复制代码

看下执行结果:

image.png

这里我们要注意的是:

flowOn 指定的是 我们 生产端 的执行线程 。从代码中我们可以看出来,我们制定了launch的所属线程为 自定义的dispatcher,所以在 执行结果可以明显看出来 emit和collet的 所属线程是不一样的

如果你看过我之前的文章你就会知道 dispatchers.io和dispatchers.default 的所属线程是一样的,所以这里为了演示上的方便 我自定义了一个dispatcher。

这里熟悉rxjava的同学 可能就会很熟悉了 是很像rxjava的 subsscribeon和observeron的

我们可以再改变一下代码:

image.png

看下执行结果:

image.png

可以看出来 当你collect2次的时候 emit 也执行了两次

总结起来就是: 在一个Flow创建出来以后,不消费则不生产,多次消费 则多次生产

所谓冷流,就是消费时才会产生的数据流,和我们的热流是相反的,热流的发送端并不依赖接收端

异常处理

发送端 如果发生了异常会咋样?

image.png

看下执行结果:

image.png

可以看出来 发送端抛出的异常,会在接收端 暴露出来,这对于接收端是有风险的。

image.png

再看下执行结果:

image.png

可以看出来 这个onComplete只能用于发现有没有异常,但他没有处理异常的能力 从输出中可以明显看到 程序发生了crash

她最大的作用是监听flow 是否结束

换一种写法:

image.png

增加了一个catch

再看结果

image.png

已经无大碍

另外要着重提一下的是,flow 没有提供取消操作 因为并不需要(可以仔细想想是为啥),如果一定要这么做,那么cancel一下 flow所属的协程即可

flow的另外几种写法

我们有时候 希望于 生产端和消费端的代码可以在一起 这样看起来处理方便。

可以采用如下的方法:

fun createFlow()=flow<Int>{
    (1..3).forEach {
        emit(it)
        delay(100)
    }
}.onEach {
    println(it)
}
fun main()= runBlocking{

    GlobalScope.launch() {
      createFlow().collect()
    }

    Thread.sleep(300000)
}
复制代码

有时候我们在生产数据的时候 可能某些时候 希望切换下协程的context

image.png

背压

这个概念看上去高大上,其实也没啥难的,无非就是你要考虑到 当生产者的速度 大大超过 消费者的时候怎么处理?

对于客户端来说 其实这种场景不会特别多, 我举个例子

聊天软件大家都用过对吧,一般背后肯定是有个长连接的,用于接收消息,接收到消息以后 我们一般都会告知 对应的ui界面 去更新界面

考虑一种场景,当一个活跃用户 时隔半年 以后打开这个app,一下子几千条消息涌入进来,如果你还是之前的 收一条消息就更新一条ui 那明显 界面会卡死的。 这里就是一个背压的典型应用场景。

可以用代码看一下:

fun main()= runBlocking{

   flow {
       List(100){
           emit(it)
       }
   }.conflate().collect {
       println("collecting $it")
       delay(100)
       println("$it collected")
   }

    Thread.sleep(300000)
}
复制代码

image.png

conflate 典型的用新数据 覆盖老数据 来解决背压的问题

此外还有一个 collectLatest 她和conflate 的区别是,只有当新数据到来的时候老数据还没有处理完,才会用新数据去覆盖老数据

文章分类
Android
文章标签