Kotlin 朱涛-21 协程 Select 选择最快的结果
目录
21 | Select:选择最快的结果
和 Kotlin 的 Channel 一样,select 在很多编程语言当中都有类似的实现,比如 Go、Rust 等。不同语言中,select 的语法可能不太一样,但背后的核心理念都是 选择更快的结果
。
select 在 Kotlin 1.6 中,仍然是一个实验性的特性(Experimental)。
选择最快的结果
需求:假如我们想查询一个商品的详情,目前有两个服务,其中缓存服务
速度快但信息可能是旧的,而网络服务
速度慢但信息是最新的。
不使用 select
data class Product(val id: String, val price: Double, val tag: Any)
fun main() = runBlocking {
val startTime = System.currentTimeMillis()
val productId = "xxxId"
suspend fun getCacheInfo(id: String): Product = delay(200L).let { return Product(id, 9.9, "cache") }
suspend fun getNetInfo(id: String): Product = delay(200L).let { return Product(id, 9.8, "net") }
fun updateUI(product: Product) = println("${product}, ${System.currentTimeMillis() - startTime}")
updateUI(getCacheInfo(productId)) // 先使用缓存
updateUI(getNetInfo(productId)) // 再查询网络
}
Product(id=xxxId, price=9.9, tag=cache), 218
Product(id=xxxId, price=9.8, tag=net), 431
存在的问题:因为 getCacheInfo()
是一个挂起函数,所以只有这个挂起函数执行完成以后,才可以继续执行后面的任务。
select + async
上面这个场景,我们可以用 async + select
来实现,async 可以实现并发,select 则可以选择最快的结果。
val product = select<Product> { // 使用 select 包裹 async 启动的两个协程,泛型代表要选择的数据类型
async { getCacheInfo(productId) }.onAwait { it } // async 两个挂起函数,可以让这两个查询并发执行
async { getNetInfo(productId) }.onAwait { it } // 使用 onAwait{} 将执行结果回传给 select{}
}
updateUI(product) // 只会打印最快的结果 Product(id=xxxId, price=9.9, tag=cache), 218
存在的问题:上面代码只会选择最快的那个结果,如果缓存是最快的话,就不会再展示最新的 net 内容。
select + async + Deferred
在上面代码的基础上,稍加修改就可以实现,先展示缓存、再展示最新的内容。
val cacheDeferred: Deferred<Product> = async { getCacheInfo(productId) } // 并发执行
val netDeferred: Deferred<Product> = async { getNetInfo(productId) } // 并发执行
val product = select<Product> { // 通过 select 选择最快的那个结果
cacheDeferred.onAwait { it }
netDeferred.onAwait { it }
}
updateUI(product)
if ("cache" == product.tag) { // 如果当前结果来自缓存,那么再取最新的网络结果更新 UI
updateUI(netDeferred.await())
}
Product(id=xxxId, price=9.9, tag=cache), 214
Product(id=xxxId, price=9.8, tag=net), 215
上面代码中,如果缓存是最快的话,就先展示缓存,再展示最新的 net 内容;否则,只展示最新的 net 内容。
使用 select 的优势
这个例子不使用 select 同样也可以实现,不过,select 这样的代码模式的优势在于,扩展性非常好。
可以看到,当增加一个缓存服务进来的时候,我们的代码只需要做很小的改动,就可以实现。
总的来说,对比传统的挂起函数串行的执行流程,select 这样的代码模式,不仅可以提升程序的整体响应速度,还可以大大提升程序的灵活性、扩展性。
选择多个结果
在协程中返回一个内容的时候,我们可以使用挂起函数、async,但如果要返回多个结果的话,就需要用到 Channel 和 Flow。
需求:假如有两个管道,现在需要将它们中的数据收集出来,并逐个打印。
不使用 select
fun main() = runBlocking {
val startTime = System.currentTimeMillis()
fun log(text: Any) = println("${text}, ${System.currentTimeMillis() - startTime}")
val channel1: ReceiveChannel<Int> = produce { (1..2).forEach { send(it);delay(100L) } }
val channel2: ReceiveChannel<String> = produce { listOf("a", "b").forEach { send(it);delay(100L) } }
log(">> start channel1")
channel1.consumeEach { log("consumeEach1 - $it") }
log(">> start channel2")
channel2.consumeEach { log("consumeEach2 - $it") } // channel1 执行完毕以后,才会执行 channel2
log("end")
}
>> start channel1, 9
consumeEach1 - 1, 33
consumeEach1 - 2, 131
>> start channel2, 244
consumeEach2 - a, 244
consumeEach2 - b, 350
end, 458
存在的问题:两个管道是串行执行的,只有当 channel1 执行完毕以后,才会执行 channel2。
select + Channel.onReceive
repeat(4) { // 重复执行 4 次 select,目的是把两个管道中的所有数据都消耗掉
select {
log(" select$it")
channel1.onReceive { log("onReceive1 - $it") } // 注意,onReceive 可能会崩溃
channel2.onReceive { log("onReceive2 - $it") } // 注意,onReceive 可能会崩溃
}
}
log("end")
select0, 15
onReceive1 - 1, 41
select1, 42
onReceive2 - a, 45
select2, 45
onReceive1 - 2, 140
select3, 140
onReceive2 - b, 156
end, 156
onReceive{}
是 Channel 在 select 当中的语法,当 Channel 中有数据以后,它就会被回调,通过这个 Lambda,我们也可以将结果传出去。
注意,上面的代码中的 onReceive 可能会导致崩溃。
onReceive 可能会导致崩溃
如果两个管道中 delay 的时间间隔较大,比如将 channel1 中 delay 值从 100 改为 1000,这时程序就会出问题了。
select0, 12
onReceive1 - 1, 39
select1, 39
onReceive2 - a, 43
select2, 43
onReceive2 - b, 158
Exception in thread "main" ClosedReceiveChannelException: Channel was closed
at channels.Closed.getReceiveException(AbstractChannel.kt:1108)
at channels.AbstractChannel$ReceiveSelect.resumeReceiveClosed(AbstractChannel.kt:989)
这是因为,Channel 中的数据发送完数据以后,就会被关闭,此时再调用 onReceive 就会触发 ClosedReceiveChannelException
异常。
select + onReceiveCatching
在 19 讲中讲过,使用 receiveCatching()
可以防止出现 ClosedReceiveChannelException
,类似的,当 Channel 与 select 配合的时候,可以使用 onReceiveCatching{}
这个高阶函数。
repeat(5) { // 可以随便重复多少次都不会崩溃
select {
log(" select$it")
channel1.onReceiveCatching { log("onReceive1 - ${it.getOrNull()}") }
channel2.onReceiveCatching { log("onReceive2 - ${it.getOrNull()}") }
}
}
log("end")
channel1.cancel() // 主动取消
channel2.cancel()
select0, 11
onReceive1 - 1, 34
select1, 35
onReceive2 - a, 38
select2, 38
onReceive2 - b, 141 // 因为 channel1 还在 delay,所以依旧会选择 channel2
select3, 141
onReceive2 - null, 249 // 如果获取的结果是空,就代表管道已经被关闭了
select4, 249
onReceive2 - null, 249
end, 249
使用 onReceiveCatching{}
后就不用担心崩溃的问题了,如果获取的结果是 null
,就代表管道已经被关闭了。
上面的代码中,我们在 repeat()
后,主动将 channel1、channel2 取消了。如果不这么做,当得到所有结果以后,程序不会立即退出。这是因为,由于 channel1 没有接收者,所以会一直 delay。
这种将
多路数据
以非阻塞的方式合并成一路数据
的模式,在其他领域也有广泛的应用,比如操作系统、Java NIO 等等。
项目实战
fun main() = runBlocking {
val startTime = System.currentTimeMillis()
fun log(text: Any) = println("${text}, ${System.currentTimeMillis() - startTime}")
suspend fun <T> getFastDeferred(vararg deferreds: Deferred<T>): T = select {
for (deferred in deferreds) {
deferred.onAwait { t: T ->
log("onAwait: $t")
deferreds.forEach { it.cancel() } // 取消其他的 Deferred
t // 返回的是最快的那个结果
}
}
}
fun <T> getDeferredAsync(result: T): Deferred<T> = async {
val delayTime = Random.nextLong(1000L)
log("Deferred: $result, delay=$delayTime")
delay(delayTime)
result
}
val list: Array<Deferred<Int>> = Array(6) { getDeferredAsync(it) }
val result: Int = getFastDeferred(*list) // 在前面加一个 * 就变成可变数组了
log("result: $result")
}
Deferred: 0, delay=703, 12
Deferred: 1, delay=129, 19
Deferred: 2, delay=679, 19
Deferred: 3, delay=318, 19
Deferred: 4, delay=58, 19 // 这个任务最快
Deferred: 5, delay=934, 19
onAwait: 4, 94
result: 4, 96
Deferred、Channel 相关 API
当 Deferred、Channel 与 select 配合的时候,它们原本的 API 会多一个 on
前缀。所以,只要记住了 Deferred、Channel 的 API,不需要额外记忆 select 的 API,只需要在原本的 API 的前面加上一个 on
就行了。
public interface Deferred : CoroutineContext.Element {
public suspend fun join()
public suspend fun await(): T
public val onJoin: SelectClause0
public val onAwait: SelectClause1<T>
}
public interface SendChannel<in E> {
public suspend fun send(element: E)
public val onSend: SelectClause2<E, SendChannel<E>>
}
public interface ReceiveChannel<out E> {
public suspend fun receive(): E
public suspend fun receiveCatching(): ChannelResult<E>
public val onReceive: SelectClause1<E>
public val onReceiveCatching: SelectClause1<ChannelResult<E>>
}
小结
select,就是选择更快的结果。
当 select 与 async、Channel 搭配以后,我们可以并发执行协程任务,以此提升程序的执行效率,并且还可以改善程序的扩展性、灵活性。
2017-03-20
本文来自博客园,作者:白乾涛,转载请注明原文链接:https://www.cnblogs.com/baiqiantao/p/6588454.html