Kotlin协程:Channel 通道

本文介绍了Kotlin协程中的Channel特性,包括其作为并发安全队列的角色,容量设定,onBufferOverflow策略,以及produce和actor的用法。同时,文章讨论了Channel的关闭机制和BroadcastChannel的广播功能。此外,还探讨了多路复用技术,如复用多个await和Channel,SelectClause的使用,以及如何通过Flow实现多路复用。最后,文章提到了并发安全问题,如不安全的并发访问和协程提供的并发工具Mutex与Semaphore,强调避免访问外部可变状态的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

脑图

1、通道

①、Channel 是什么

Channel 实际上是一个并发安全的队列,他可以用来连接协程,实现不同的协程的通讯。

在这里插入图片描述

@Test
fun channelStudy() = runBlocking {
    val channel = Channel<Int>()
    // 生产者
    val producer = GlobalScope.launch {
        var i = 1
        while (true) {
            delay(1000)
            println("生产 sending $i")
            channel.send(i++)
        }
    }
    // 消费者
    val consumer = GlobalScope.launch {
        while (true) {
            val element = channel.receive()
            println("消费 receive: $element")
        }
    }
    joinAll(producer, consumer)
}

//执行输出
生产 sending 1
消费 receive: 1
生产 sending 2
消费 receive: 2
生产 sending 3
消费 receive: 3
生产 sending 4
消费 receive: 4
生产 sending 5
消费 receive: 5
...

生产者每隔一秒生产一个元素,然后立刻被消费者消费掉

②、Channel 的容量

Channel实际上就是一个队列,队列中一定存在缓冲区,那么一旦这个缓冲区满了,并且也一直没有人调用receive并取走元素,send就需要挂起。

public fun <E> Channel(
    capacity: Int = RENDEZVOUS,
    onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND,
    onUndeliveredElement: ((E) -> Unit)? = null
): Channel<E> 

//Channel 默认有一个容量大小RENDEZVOUS,值为0
public const val RENDEZVOUS: Int = 0

若故意让接收端的节奏放慢,发现send总是会挂起,直到receive之后才会继续往下执行。

@Test
fun channelStudy() = runBlocking {
    val channel = Channel<Int>()
    val start = System.currentTimeMillis()
    // 生产者
    val producer = GlobalScope.launch {
        var i = 1
        while (true) {
            delay(1000)
            println("生产 sending $i, ${System.currentTimeMillis() - start}")
            channel.send(i++)
        }
    }
    // 消费者
    val consumer = GlobalScope.launch {
        while (true) {
            delay(5000)
            val element = channel.receive()
            println("消费 receive: $element, ${System.currentTimeMillis() - start}")
        }
    }
    joinAll(producer, consumer)
}

//执行输出
生产 sending 1, 1013
消费 receive: 1, 5010
生产 sending 2, 6013
消费 receive: 2, 10012
生产 sending 3, 11013
...

由于缓冲区默认为0,所以生产者每次不得不等待消费者消费掉元素后再生产。

避免 send一直频繁
如果将缓存去设置成比生成的数据长,那么生产者就会一下子把元素生成完发送出来,消费者再按照自己的节奏进行消费。这样的话就可以避免 send一直频繁的挂起。

###③、 迭代Channel
Channel本身像序列,在读取的时候,可以直接获取一个Channel的iterator。

@Test
fun channelStudy() = runBlocking {
    //设置一个缓存大小为Int.MAX_VALUE的通道
    val channel = Channel<Int>(Channel.UNLIMITED)
    val start = System.currentTimeMillis()
    // 生产者
    val producer = GlobalScope.launch {
        for (i in 1..5) {
            println("生产 sending $i, ${System.currentTimeMillis() - start}")
            channel.send(i)
        }
    }
    // 消费者
    val consumer = GlobalScope.launch {
        val it = channel.iterator()
        while (it.hasNext()) {
            val element = it.next()
            println("消费 receive: $element, ${System.currentTimeMillis() - start}")
            delay(2000)
        }
    }
    joinAll(producer, consumer)
}

//执行输出
生产 sending 1, 7
生产 sending 2, 16
生产 sending 3, 21
生产 sending 4, 21
生产 sending 5, 21
消费 receive: 1, 23
消费 receive: 2, 2028
消费 receive: 3, 4032
消费 receive: 4, 6033
消费 receive: 5, 8034

将消费者代码更换成 for 循环也是一样的

// 消费者
val consumer = GlobalScope.launch {
    for (element in channel){
        println("消费 receive: $element, ${System.currentTimeMillis() -start}")
    }
}

容量说明
Channel.RENDEZVOUS默认值,发送后等待接收
Channel.CONFLATED无限容量
Channel.UNLIMITED容量为1,新数据会替代旧数据
Channel.BUFFERED缓存容量,默认情况下是64,具体容量可以设置VM参数

③、onBufferOverflow

当管道指定容量后,管道的容量满了室,Channel的应对策略。

策略说明
BufferOverflow.SUSPEND默认值,当管道的容量满了后,如果发送方还再继续发送,就会挂起send()方法,等管道空闲了后再恢复
BufferOverflow.DROP_OLDEST丢弃最旧的数据,然后发送新数据
BufferOverflow.DROP_LATEST丢弃最新的数据,管道的内容维持不变

④、produce 与 actor

  • 构造生产者与消费者的便捷方法
  • 可以通过produce方法启动一个生产者协程,并返回一个ReceiveChannel,其他协程就可以用这个Channel来接收数据了。反过来,可以用actor启动一个消费者协程。

produce方式

@Test
fun testProducer() = runBlocking {
    val receiveChannel = GlobalScope.produce(capacity = 50) {
        repeat(5) {
            delay(100)
            println("生产 produce $it")
            send(it)
        }
    }
    val consumer = GlobalScope.launch {
        for (i in receiveChannel) {
            delay(300)
            println("消费 consume: $i")
        }
    }
    consumer.join()
}

//执行输出
生产 produce 0
生产 produce 1
生产 produce 2
消费 consume: 0
生产 produce 3
生产 produce 4
消费 consume: 1
消费 consume: 2
消费 consume: 3
消费 consume: 4

actor方式 : actor 创建的是消费者,别搞混了

@Test
fun testActor() = runBlocking<Unit> {
    val sendChannel = GlobalScope.actor<Int> {
        while (true) {
            val element = receive()
            println("消费 receive: $element")
        }
    }

    val producer = GlobalScope.launch {
        for (i in 1..3) {
            println("生产 send: $i")
            sendChannel.send(i)
        }
    }

    producer.join()
}

//执行输出
生产 send: 1
生产 send: 2
消费 receive: 1
生产 send: 3
消费 receive: 2
消费 receive: 3

⑤、Channel 的关闭

  • produce和actor返回的channel都会随着对应的协程执行完毕而关闭,也正是如此,Channel才被称为热数据流
  • 对于一个Channel,如果调用了它的close方法,会立即停止接收新元素,也就是说,这时它的isClosedForSend会立即返回true。而由于Channel缓冲区的存在,这时候可能还有一些元素没有被处理完,因此要等缓冲区中所有的元素被读取之后isClosedForReceive才会返回true。
  • Channel的生命周期最好由主导方来维护,建议由主导的一方实现关闭。
@Test
fun testClose() = runBlocking<Unit> {
    val channel = Channel<Int>(3)
    val producer = GlobalScope.launch {
        List(3) {
            channel.send(it)
        }
        channel.close()
        //如果调用了它的close方法,会立即停止接收新元素
        println("close channel. closeForSend: ${channel.isClosedForSend}, closeFoReceive: ${channel.isClosedForReceive}")
    }
    val consumer = GlobalScope.launch {
        for (e in channel) {
            delay(1000)
        }
        println("after consuming. closeForSend: ${channel.isClosedForSend}, closeFoReceive: ${channel.isClosedForReceive}")
    }
    joinAll(producer, consumer)
}

//执行输出
close channel. closeForSend: true, closeFoReceive: false
after consuming. closeForSend: true, closeFoReceive: true

⑥、广播 BroadcastChannel

发送端和接收端在Channel中存在一对多的情形,从数据处理本身来讲,虽然有多个接收端,但是同一个元素只会被一个接收端读到。广播则不然,多个接收端不存在互斥行为。

创建一个BroadcastChannel,广播数据,开启多个协程订阅广播,每个协程都能接收到广播数据。

@Test
fun testBroadcast() = runBlocking {
    val broadcastChannel = BroadcastChannel<Int>(Channel.BUFFERED)
    val producer = GlobalScope.launch {
        List(3) {
            delay(100)
            broadcastChannel.send(it)
        }
        broadcastChannel.close()
    }
    List(3) { index ->
        GlobalScope.launch {
            val receiveChannel = broadcastChannel.openSubscription()
            for (i in receiveChannel) {
                println("#$index received $i")
            }
        }
    }.joinAll()
}
//执行输出
#1 received 0
#2 received 0
#0 received 0
#0 received 1
#2 received 1
#1 received 1
#0 received 2
#2 received 2
#1 received 2

Channel可以转换成BroadcastChannel,实现的效果与上例一样:

//val broadcastChannel = BroadcastChannel<Int>(Channel.BUFFERED)
val channel = Channel<Int>()
val broadcastChannel = channel.broadcast(Channel.BUFFERED)

2、多路复用

数据通信系统或者计算机网络系统中,传输媒体的带宽或容量往往大于传输单一信号的需求,为了有效地利用通信线路,希望一个信道同时传输多路信号,这就是多路复用技术(Multiplexing)。

①、复用多个 await

模拟分别从网络和本地缓存获取数据,实现哪个先返回就先用哪个做展示。

多路复用

private suspend fun CoroutineScope.getUserFromLocal(name: String) = async {
    delay(1000)
    "local $name"
}

private suspend fun CoroutineScope.getUserFromRemote(name: String) = async {
    delay(2000)
    "remote $name"
}

data class Response<T>(val value: T, val isLocal: Boolean = false)

@Test
fun testSelectAwait() = runBlocking<Unit> {
    GlobalScope.launch {
        val localUser = getUserFromLocal("Jack")
        val remoteUser = getUserFromRemote("Mike")
        val userResponse = select<Response<String>> {
            localUser.onAwait { Response(it, true) }
            remoteUser.onAwait { Response(it, false) }
        }
        println("render on UI: ${userResponse.value}")
    }.join()
}

//执行输出
render on UI: local Jack

local方法延迟时间短,先返回了,所以select选取的是local数据。

②、复用多个 Channel

跟await类似,会接收到发送最快的那个Channel消息。

@Test
fun testSelectChannel() = runBlocking<Unit> {
    val channels = listOf(Channel<Int>(), Channel<Int>())
    GlobalScope.launch {
        delay(100)
        channels[0].send(100)
    }
    GlobalScope.launch {
        delay(50)
        channels[1].send(50)
    }
    val result = select<Int?> {
        channels.forEach { channel -> channel.onReceive { it } }
    }
    println("result $result")
    delay(1000)
}

//执行输出
result 50

两个Channel发送数据元素,第一个延迟100毫秒发送,第二个延迟50毫秒发送,用select接收的时候,收到延迟50毫秒发送的那个。

③、SelectClause

怎么知道哪些事件可以被select呢?其实所有能被select的事件都是SelectClauseN类型,包括:

  1. SelectClause0: 对应事件没有返回值,例如join没有返回值,那么onJoin就是SelectClauseN类型,使用时,onJoin的参数是一个无参函数。
  2. SelectClause1: 对应事件有返回值,前面的onAwait和onReceive都是类似情况。
  3. SelectClause2: 对应事件有返回值,此外还需要一个额外的参数,例如Channel.onSend有两个参数,第一个是Channel数据类型的值,表示即将发送的值,第二个是发送成功的回调参数。

如果想要确认挂起函数时否支持select,只需要查看其是否存在对应的SelectClauseN类型可回调即可

@Test
fun testSelectClause0() = runBlocking<Unit> {
    val job1 = GlobalScope.launch {
        delay(100)
        println("job 1")
    }

    val job2 = GlobalScope.launch {
        delay(10)
        println("job 2")
    }
    select<Unit> {
        job1.onJoin { println("job 1 onJoin") }
        job2.onJoin { println("job 2 onJoin") }
    }
    delay(1000)
}

//执行输出
job 2
job 2 onJoin
job 1

启动两个协程job1和job2,job2延迟少,先打印,所以select中选择了job2 (打印了job 2 onJoin),因为两个协程没有返回值,或者说返回值是Unit,所以在select后面声明。

示例:两个参数的例子,第一个是值,第二个是回调参数。

@Test
fun testSelectClause2() = runBlocking<Unit> {
    val channels = listOf(Channel<Int>(), Channel<Int>())
    println(channels)
    launch(Dispatchers.IO) {
        select<Unit> {
            launch {
                delay(10)
                channels[1].onSend(10) { sendChannel ->
                    println("sent on $sendChannel")
                }
            }
            launch {
                delay(100)
                channels[0].onSend(100) { sendChannel ->
                    println("sent on $sendChannel")
                }
            }
        }
    }
    GlobalScope.launch {
        println(channels[0].receive())
    }
    GlobalScope.launch {
        println(channels[1].receive())
    }
    delay(1000)
}

//执行输出
[RendezvousChannel@273e7444{EmptyQueue}, RendezvousChannel@2d52216b{EmptyQueue}]
10
sent on RendezvousChannel@2d52216b{EmptyQueue}

启动两个协程,分别延迟10毫秒和100毫秒,分别使用onSend发送数据10和100,第二个参数是回调;
然后启动两个协程分别接收两个Channel对象发送的数据,可以看到结果选择了较少延迟的那个协程。

看源码 onJoin、onSend 都有SelectClauseN接口,所以支持select

public val onSend: SelectClause2<E, SendChannel<E>>

public val onJoin: SelectClause0

④、使用Flow实现多路复用

多少情况下,可以通过构造合适的Flow来实现多路复用的效果

fun CoroutineScope.getUserFromLocal(name: String) = async {
    delay(1000)
    "local $name"
}

fun CoroutineScope.getUserFromRemote(name: String) = async {
    delay(2000)
    "remote $name"
}

data class Response<T>(val value: T, val isLocal: Boolean = false)

class ChannelTest {

    @Test
    fun testSelectFlow() = runBlocking<Unit> {
        // 函数 -> 协程 -> Flow -> Flow合并
        val name = "guest"
        coroutineScope {
            coroutineScope {
                listOf(::getUserFromLocal, ::getUserFromRemote) // 函数
                    .map { function -> function.call(name) }    // 协程
                    .map { deferred ->
                        flow { emit(deferred.await()) }         // Flow
                    }
                    .merge()                                    // Flow 合并
                    .collect { user -> println("collect $user") }
            }
        }
    }
}

//执行输出
collect local guest
collect remote guest

3、并发安全

①、不安全的并发访问

使用线程在解决并发问题的时候总是会遇到线程安全的问题,而Java平台上的Kotlin协程实现免不了存在并发调度的情况,因此线程安全同样值得留意。

@Test
fun testUnsafeConcurrent() = runBlocking<Unit> {
    var count = 0
    List(1000) {
        GlobalScope.launch { count++ }
    }.joinAll()
    println(count)
}

//执行输出
997

启动1000个协程对count进行加一操作,运行多次,可能每次结果都不一样,有的协程拿到count后加操作还没完成,就被别的协程进行加操作了。所以同样需要注意线程安全。

使用AtomicInteger 解决并发问题,这个Java线程提供的解决方案

@Test
fun testSafeConcurrent() = runBlocking<Unit> {
    var count = AtomicInteger(0)
    List(1000) {
        GlobalScope.launch { count.incrementAndGet() }
    }.joinAll()
    println(count.get())
}

//执行输出
1000

②、协程的并发工具

除了在线程中常用的解决并发问题的手段外,协程框架也提供了一些并发安全地工具:

  • Channel:并发安全地消息通道。这个已经熟悉。
  • Mutex: 轻量级锁,它的lock和unlock从语义上与线程锁比较类似,之所以轻量级是因为它在获取不到锁时不会阻塞线程,而是挂起等待锁的释放。
  • Semaphore: 轻量级信号量,信号量可以有多个,协程在获取到信号量后即可执行并发操作。当Semaphore的参数为1时,效果等价于Mutex。

示例:Mutex轻量级锁

@Test
fun testMutex() = runBlocking<Unit> {
    var count = 0
    val mutex = Mutex()
    List(1000) {
        GlobalScope.launch {
            mutex.withLock {
                count++
            }
        }
    }.joinAll()
    println(count)
}

//执行输出
1000

示例:Semaphore轻量级信号量

@Test
fun testSemaphore() = runBlocking<Unit> {
    var count = 0
    val semaphore = Semaphore(1)
    List(1000) {
        GlobalScope.launch {
            semaphore.withPermit {
                count++
            }
        }
    }.joinAll()
    println(count)
}

③、避免访问外部可变状态

编写函数的时候,要求它不得访问外部状态,只能基于参数做运算,通过返回值提供运算结果。

示例

@Test
fun testAvoidAccessOuter() = runBlocking<Unit> {
    val count = 0
    val result = count + List(1000) {
        GlobalScope.async { 1 }
    }.map { it.await() }.sum()
    println(result)
}

这个是一个比较极端的例子,把count作为外部变量,和协程隔开,也就不存在并发的安全性问题了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值