携手创作,共同成长!这是我参与「掘金日新计划 · 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,本篇就此完结了。