• 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

  • 相关阅读:
    iis 5.0 不能运行 asp.net 的 原因
    股票在线讨论
    adsl 加 路由器 网关不能上网的 原因
    汽车英语
    势与子的辩证法
    中国上海的黑心培训学校
    excel 里的 图表的使用
    带宽 下载速度 比特 率 换算 速度
    硬盘分区表修复秘籍
    Bootstrap a标签的单击时停止传播事件
  • 原文地址:https://www.cnblogs.com/baiqiantao/p/6588454.html
Copyright © 2020-2023  润新知