Kotlin协程-协程之间的通信(Channel)与广播(BroadcastChannel)

1,300 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情

Kotlin协程-协程之间的通信与广播

Kotlin协程基本套餐:

在协程的应用中,其实并发很常见,很大数量是的并发少见,很大数量的并发中还要操作同一个数据保证原子性那是更加的少见,之前的文章我们讲了并发与并发安全的解决方案。

除了并发安全没有那么常见,其实协程的通信我们也没有那么常见,但是我们理解了这些点之后,我们就能很方便的实现一些特定的功能,我可以不会,我可以不用,但是我不能不知道!是不是这个理。

一、协程之间的通信

协程之间还能通信?有些同学可能根本不知道这个概念,是不是使用消息总线 RxBus 那种?

其实协程已经给我提供一个消息通信类 Channel ,Channel 是一种并发安全的队列,用来连接协程,实现协程间通信。

直接举例:

      runBlocking {
            val channel = Channel<Int>()

            async(Dispatchers.IO) {
                repeat(5) {
                    channel.send(it)
                }
            }

            async(Dispatchers.IO) {
                repeat(5) {
                    val result = channel.receive()
                    YYLogUtils.w("result: $result")
                }
            }

        }

一个协程直接发送数据,另一个协程可以接收数据

之前说到 Chanel 是一个队列,上面我们通过遍历多个协程来接收,那么我们同样的可以通过一个协程来遍历 Chanel 来接收数据

 runBlocking {
            val channel = Channel<Int>()

            async(Dispatchers.IO) {
                repeat(5) {
                    channel.send(it)
                }
            }

            async(Dispatchers.IO) {
                val iterator = channel.iterator()
                while (iterator.hasNext()) {
                    val result = iterator.next()
                    YYLogUtils.w("result: $result")
                }
            }

        }

效果和上面是一样的。

这里需要注意的是 Channel 的内部有一个缓冲区,如果缓冲区满了,receive() 方法还没有被调用,那么 send() 方法就会挂起,直到缓冲区中的元素被 receive() 函数取走后再继续执行。

比如我们设置 Channel 的缓冲区为3 那么就会chua chua chua 先 send 3个再说。如果我们没有设置缓冲区那么就会等对方接收了才会 send 下一个。

 runBlocking {
            val channel = Channel<Int>()

             async(Dispatchers.IO) {
                repeat(5) {
                    YYLogUtils.w("start-开始Send")
                    channel.send(it)
                }
            }

            async(Dispatchers.IO) {
                repeat(5) {
                    delay(1000)
                    val result = channel.receive()
                    YYLogUtils.w("end-result: $result")
                }
            }

        }

可以看到没有设置缓冲区,是send一个 接收一个 然后才能再 send 。

那我们现在设置缓冲区为3,看看效果

 runBlocking {
            val channel = Channel<Int>(3)

             async(Dispatchers.IO) {
                repeat(5) {
                    YYLogUtils.w("start-开始Send")
                    channel.send(it)
                }
            }

            async(Dispatchers.IO) {
                repeat(5) {
                    delay(1000)
                    val result = channel.receive()
                    YYLogUtils.w("end-result: $result")
                }
            }

        }

那么send的时候就有三个缓存了,效果如下

二、协程之间的广播

普通的 Channel 中的元素只能被接收一次,如果需要多次接收,就需要用到 BroadcastChannel,BroadcastChannel 是一种广播机制的 Channel

        runBlocking {
            val broadcastChannel = BroadcastChannel<Int>(Channel.BUFFERED)

            repeat(3) {
                async {
                    val receiveChannel = broadcastChannel.openSubscription()
                    for (result in receiveChannel) {
                        YYLogUtils.w("result: $result")
                    }
                }
            }

            async {
                repeat(5) {
                    YYLogUtils.w("开始Send")
                    broadcastChannel.send(it)
                }
                broadcastChannel.close()
            }

        }

代码大家应该都能看懂,我开启了一个协程,发出了5个数字,然后我开启了3个协程,打印接收到值。

如果要实现广播的效果,那么就是0-4每一个值我每一个接收协程都需要打印一遍,那么就是3遍 :

可以看到3个接收的协程都能收到接收的数据,如果我们只是使用 Channel ,那么每一个数字发出来,我们只能接收一次。

三、协程的优先级

场景如下,如果有多个协程并发执行,我想找到其中先执行完的那一个,我该如何操作,发送事件消息?可以,发送协程消息?可以!不过我们有更简单的方法,协程提供了 select 函数,可以让我们找到并发线程先执行完的那一个协程。

先上一个简单的示例:

    runBlocking {

            val data1 = async {
                delay(1000)
                YYLogUtils.w("第一个协程完成")
            }

            val data2 = async {
                delay(2000)
                YYLogUtils.w("第二个协程完成")
            }

            select<Unit> {
               data1.onAwait{
                   YYLogUtils.w("我选了第一个协程")
                }
                data2.onAwait {
                    YYLogUtils.w("我选了第二个协程")
                }
            }


        }

打印结果如下:

我们看看select函数:

public suspend inline fun <R> select(crossinline builder: SelectBuilder<R>.() -> Unit): R {

需要参数 SelectBuilder ,我们看看 SelectBuilder

public interface SelectBuilder<in R> {
  
    public operator fun SelectClause0.invoke(block: suspend () -> R)
   
    public operator fun <Q> SelectClause1<Q>.invoke(block: suspend (Q) -> R)

    public operator fun <P, Q> SelectClause2<P, Q>.invoke(param: P, block: suspend (Q) -> R)

    public operator fun <P, Q> SelectClause2<P?, Q>.invoke(block: suspend (Q) -> R): Unit = invoke(null, block)

他需要 SelectClause0 SelectClause1 SelectClause2

我们再看看哪些方法函数返回这些,常用的几个如下:

Job对象的

public val onJoin: SelectClause0

Deferred对象的

public val onAwait: SelectClause1<T>

Channel的

public val onReceive: SelectClause1<E>

我们把上面的 async 改为 launch 试试 Job 的选中

       runBlocking {

            val data1 = launch {
                delay(1000)
                YYLogUtils.w("第一个协程完成")
            }

            val data2 = launch {
                delay(2000)
                YYLogUtils.w("第二个协程完成")
            }

            select<Unit> {
               data1.onJoin{
                   YYLogUtils.w("我选了第一个协程")
                }
                data2.onJoin {
                    YYLogUtils.w("我选了第二个协程")
                }
            }
            
        }

使用 onJoin 一样的可以达到上面 async 的效果。

下面我们看看使用 Channel 的选中效果,这里的 produce 函数是快速创建生产者的协程,专门用于发送Channel的一个便捷方法。

        runBlocking {

            val channel1: ReceiveChannel<Int> = produce<Int> {
                delay(2000)
                send(1)
                YYLogUtils.w("发送消息完成1")
            }

            val channel2: ReceiveChannel<Int> = produce<Int> {
                delay(1000)
                send(2)
                YYLogUtils.w("发送消息完成2")
            }

            select<Unit> {
                channel1.onReceive {
                    YYLogUtils.w("我选了第一个协程")
                }
                channel2.onReceive {
                    YYLogUtils.w("我选了第二个协程")
                }
            }

        }

打印效果如下:

就只会接收到先完成的那一个协程消息。与 async/launch 那种方式有所不同,async/launch 的方式不管是否 select 都会执行完协程,而 Channel 的方式,只会收到先完成协程的消息。利用它们的不同点我们就能完成一些特定的功能。

总结

协程的基本概念,常见与不常见的一些使用方法就介绍到这里了。

如果大家看的过程有不明白的我更推荐你从系列的第一篇开始看,内部概念与难度是一步一步层层递进的。

如果感觉本文对你有一点点点的启发,还望你能点赞支持一下,你的支持是我最大的动力。

Ok,本篇就此完结了。