认识Channel
Channel 实际上是一个并发安全的队列,它可以用来连接协程,实现不同协程的通信。
使用Channel进行协程通信例子如下:
suspend fun main() {
val channel = Channel<Int>()
val producer = GlobalScope.launch {
var i = 0
while (true){
channel.send(i++)
println("send $i")
delay(1000)
}
}
val consumer = GlobalScope.launch {
while(true){
val element = channel.receive()
println("receive $element")
}
}
joinAll(producer, consumer)
}
在这个例子中,producer 当中每隔 1s 向 Channel 中发送一个数字,而 consumer 那边则是一直在读取 Channel 来获取这个数字并打印,我们能够发现这里发端是比收端慢的,在没有值可以读到的时候,receive 是挂起的,直到有新元素 send 过来。
Channel 的容量
Channel 实际上就是一个队列,队列中一定存在缓冲区,那么一旦这个缓冲区满了,并且一直没有人调用 receive 并取走函数,send 就需要挂起,故意让接收端节奏放慢,发现send总是会挂起,直到 receive 之后才会继续往下执行。
我们来看下 Channel 的缓冲区的定义:
public fun <E> Channel(capacity: Int = RENDEZVOUS): Channel<E> =
when (capacity) {
RENDEZVOUS -> RendezvousChannel()
UNLIMITED -> LinkedListChannel()
CONFLATED -> ConflatedChannel()
BUFFERED -> ArrayChannel(capacity)
}
它有一个参数叫 capacity,指定缓冲区的容量,默认值 RENDEZVOUS 就是 0,如果 consumer 不 receive,producer 里面的第一个 send 就给挂起了,例子如下:
val producer = GlobalScope.launch {
var i = 0
while (true){
println("before send $i")
channel.send(i++)
println("after send $i")
delay(1000)
}
}
val consumer = GlobalScope.launch {
while(true){
delay(2000)
val element = channel.receive()
println("receive $element")
}
}
//before send 1
//receive 1
//after send 1
//before send 2
//receive 2
//after send 2
//before send 3
//receive 3
//after send 3
UNLIMITED 指缓冲区没有限制,send 可以不断的发出数据放到缓冲区内,receive 一个一个去取。CONFLATED 指如果 send 发出了 [1,2,3,4,5] 五个元素,这个时候如果调用了 receive,则取出的是 5。换句话说,这个类型的 Channel 有一个元素大小的缓冲区,但每次有新元素过来,都会用新的替换旧的,也就是说我发了个 1、2、3、4、5 之后收端才接收的话,就只能收到 5 了。BUFFERED 它接收一个值作为缓冲区容量的大小,如果缓冲区达到了上限,则 send 会被挂起。
迭代 Channel
Channel 本身实际上也有点儿像序列,可以一个一个读,所以我们在读取的时候也可以直接获取一个 Channel 的 iterator:
val producer = GlobalScope.launch {
for (x in 1..5) {
channel.send(x * x)
println("send ${x * x}")
}
}
val consumer = GlobalScope.launch {
val iterator = channel.iterator()
while (iterator.hasNext()) {
val element = iterator.next()
println("receive $element")
delay(2000)
}
}
//send 1
//send 4
//send 9
//receive 1
//receive 4
//receive 9
这个写法自然可以简化成 for each:
for (element in channel) {
println("receive $element")
delay(2000)
}
produce 和 actor
上面我们实现了一个简单的生产-消费者的示例,那么有没有便捷的办法构造生产者和消费者呢?
suspend fun main() {
val receiveChannel: ReceiveChannel<Int> = GlobalScope.produce {
repeat(100) {
delay(1000)
send(it)
}
}
val consumer = GlobalScope.launch {
for(i in receiveChannel) {
println("received: $i")
}
}
consumer.join()
}
//received: 0
//received: 1
//received: 2
//received: 3
我们可以通过 produce 方法启动一个生产者协程,并返回一个 ReceiveChannel,其他协程就可以用这个 Channel 来接收数据了,反过来,我们可以用 actor 启动一个消费者协程。
val sendChannel: SendChannel<Int> = GlobalScope.actor<Int> {
while(true){
val element = receive()
println(element)
}
}
val producer = GlobalScope.launch {
for (i in 0..3) {
sendChannel.send(i)
}
}
producer.join()
//0
//1
//2
//3
ReceiveChannel 和 SendChannel 都是 Channel 的父接口,前者定义了 receive,后者定义了 send,Channel 也因此既可以 receive 又可以 send。
Channel 的关闭
produce 和 actor 返回的 Channel 都会伴随着对应的协程执行完毕而关闭,也正是这样,Channel 才被称为热数据流。
对于一个 Channel,如果我们调用了它的 close,它会立即停止接受新元素,也就是说这时候它的 isClosedForSend 会立即返回 true,而由于 Channel 缓冲区的存在,这时候可能还有一些元素没有被处理完,所以要等所有的元素都被读取之后 isClosedForReceive 才会返回 true。
Channel 的生命周期最好由主导方来维护,建议由主导的一方实现关闭。
suspend fun main() {
val channel = Channel<Int>(3)
val producer = GlobalScope.launch {
List(3) {
channel.send(it)
println("send $it")
}
channel.close()
println("close channel. ClosedForSend = ${channel.isClosedForSend} ClosedForReceive = ${channel.isClosedForReceive}")
}
val consumer = GlobalScope.launch {
for (element in channel) {
println("receive: $element")
delay(1000)
}
println("After Consuming. ClosedForSend = ${channel.isClosedForSend} ClosedForReceive = ${channel.isClosedForReceive}")
}
joinAll(producer, consumer)
}
//send 0
//send 1
//send 2
//receive: 0
//close channel. ClosedForSend = true ClosedForReceive = false
//receive: 1
//receive: 2
//After Consuming. ClosedForSend = true ClosedForReceive = true
BroadcastChannel
发送端和接收端在 Channel 中存在一对多的情形,从数据处理本身来讲,虽然有多个接收端,但是同一个元素只会被一个接收端读到。广播则不然,多个接收端不存在互斥行为。
suspend fun main() {
val broadcastChannel = BroadcastChannel<Int>(Channel.BUFFERED)
val producer = GlobalScope.launch {
List(3) {
delay(1000)
broadcastChannel.send(it)
println("send $it")
}
broadcastChannel.close()
}
List(3) { index ->
GlobalScope.launch {
val receiveChannel = broadcastChannel.openSubscription()
for (element in receiveChannel) {
println("[$index] receive: $element")
delay(1000)
}
}
}.joinAll()
producer.join()
}
//send 0
//[0] receive: 0
//[1] receive: 0
//[2] receive: 0
//send 1
//[2] receive: 1
//[0] receive: 1
//[1] receive: 1
//send 2
//[1] receive: 2
//[0] receive: 2
//[2] receive: 2
除了直接创建以外,我们也可以直接用前面定义的普通的 Channel 来做个转换:
//val broadcastChannel = BroadcastChannel<Int>(Channel.BUFFERED)
val channel = Channel<Int>()
val broadcastChannel = channel.broadcast(3)
Select
数据通信系统或者计算机网络系统中,传输媒体的带宽或容量往往会大于传输单一信号的需求,为了有效地利用通信线路,希望一个信道同时传输多路信号,这就是所谓的多路复用技术。
复用多个 await
有这样一个场景,两个 API 分别从网络和本地缓存获取数据,期望哪个先返回就先用哪个做展示:
//private val cachePath = "E://coroutine.cache"
//private val gson = Gson()
//data class Response<T>(val value: T, val isLocal: Boolean)
//...
suspend fun getUserFromLocal(name: String) = GlobalScope.async(Dispatchers.IO) {
delay(1000)
File(cachePath).readText().let { gson.fromJson(it, User::class.java) }
}
suspend fun getUserFromRemote(name: String) = GlobalScope.async(Dispatchers.IO) {
userServiceApi.getUser(name)
}
不管先调用哪个 API 返回的 Deferred 的 await,都会被挂起,如果想要实现这一需求就要启动两个协程来调用 await,这样反而将问题复杂化了。接下来我们用 select 来解决这个问题:
GlobalScope.launch {
val localDeferred = getUserFromLocal(login)
val remoteDeferred = getUserFromLocal(login)
val userResponse = select<Response<User?>> {
localDeferred.onAwait { Response(it, true) }
remoteDeferred.onAwait { Response(it, false) }
}
...
}.join()
我们没有直接调用 await,而是调用了 onAwait 在 select 当中注册了个回调,不管哪个先回调,select 立即返回对应回调中的结果。假设 localDeferred.onAwait 先返回,那么 userResponse 的值就是 Response(it, true),当然由于我们的本地缓存可能不存在,因此 select 的结果类型是 Response<User?>。
对于这个案例本身,如果先返回的是本地缓存,那么我们还需要获取网络结果来展示最新结果:
GlobalScope.launch {
...
userResponse.value?.let { log(it) }
userResponse.isLocal.takeIf { it }?.let {
val userFromApi = remoteDeferred.await()
cacheUser(login, userFromApi)
log(userFromApi)
}
}.join()
复用多个 Channel
suspend fun main() {
val channels = listOf(Channel<Int>(), Channel<Int>())
GlobalScope.launch {
delay(100)
channels[0].send(200)
}
GlobalScope.launch {
delay(50)
channels[0].send(100)
}
val result = select<Int?> {
channels.forEach { channel ->
channel.onReceive { it }
}
}
println(result)
}
// 100
SelectClause
我们怎么知道哪些事件可以被 select 呢?其实所有能够被 select 的事件都是 SelectClauseN 类型,包括:
-
SelectClause0:对应事件没有返回值,例如join没有返回值,对应的onJoin就是这个类型,使用时onJoin的参数是一个无参函数:suspend fun main() { val job1 = GlobalScope.launch { delay(100) println("job 1") } val job2 = GlobalScope.launch { delay(10) println("job 2") } select { job1.onJoin { println("job1 onJoin") } job2.onJoin { println("job2 onJoin") } } delay(1000) // job 2 // job2 onJoin // job 1 }
-
SelectClause1:对应事件有返回值,前面的onAwait和onReceive都是此类情况。 -
SelectClause2:对应事件有返回值,此外还需要额外的一个参数,例如Channel.onSend有两个参数,第一个就是一个Channel数据类型的值,表示即将发送的值,第二个是发送成功时的回调:suspend fun main() { val channels = listOf(Channel(), Channel()) println(channels) GlobalScope.launch(Dispatchers.IO) { select<Unit?> { launch { delay(10) channels[1].onSend(200) { sendChannel -> println("send on sendChannel") } } launch { delay(100) channels[0].onSend(100) { sendChannel -> println("send on sendChannel") } } } } GlobalScope.launch { println(channels[0].receive()) } GlobalScope.launch { println(channels[1].receive()) } delay(1000) // [RendezvousChannel@3712b94{EmptyQueue}, RendezvousChannel@536aaa8d{EmptyQueue}] // 200 // send on RendezvousChannel@536aaa8d{EmptyQueue} }
在消费者的消费效率较低时,数据能发给哪个就发给哪个进行处理,onSend 的第二个参数的参数是数据成功发送到的 Channel 对象。