Channel 和 Select

173 阅读4分钟

认识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,而是调用了 onAwaitselect 当中注册了个回调,不管哪个先回调,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:对应事件有返回值,前面的 onAwaitonReceive 都是此类情况。

  • 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 对象。