破解 Kotlin 協程(10) - Select 篇

Select 並非什麼新鮮概念,咱們在 IO 多路複用的時候就見過它,在 Java NIO 裏面也見過它。接下來給各位介紹的是 Kotlin 協程的 Select。java

複用多個 await

咱們前面已經接觸了不少掛起函數,那麼若是我有這樣一個場景,兩個 API 分別從網絡和本地緩存獲取數據,指望哪一個先返回就先用哪一個作展現:git

fun CoroutineScope.getUserFromApi(login: String) = async(Dispatchers.IO){
        gitHubServiceApi.getUserSuspend(login)
    }
    
    fun CoroutineScope.getUserFromLocal(login:String) = async(Dispatchers.IO){
        File(localDir, login).takeIf { it.exists() }?.readText()?.let { gson.fromJson(it, User::class.java) }
    }
複製代碼

無論先調用哪一個 API 返回的 Deferredawait,都會被掛起,若是想要實現這一需求就要啓動兩個協程來調用 await,這樣反而將問題複雜化了。編程

接下來咱們用 select 來解決這個問題:緩存

GlobalScope.launch {
        val localDeferred = getUserFromLocal(login)
        val remoteDeferred = getUserFromApi(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?>網絡

對於這個案例自己,若是先返回的是本地緩存,那麼咱們還須要獲取網絡結果來展現最新結果:async

GlobalScope.launch {
    ...
    userResponse.value?.let { log(it) }
    userResponse.isLocal.takeIf { it }?.let {
        val userFromApi = remoteDeferred.await()
        cacheUser(login, userFromApi)
        log(userFromApi)
    }
}.join()
複製代碼

複用多個 Channel

對於多個 Channel 的狀況,也比較相似:函數

val channels = List(10) { Channel<Int>() }

select<Int?> {
    channels.forEach { channel ->
        channel.onReceive { it }
        // OR
        channel.onReceiveOrNull { it }
    }
}
複製代碼

對於 onReceive,若是 Channel 被關閉,select 會直接拋出異常;而對於 onReceiveOrNull 若是遇到 Channel 被關閉的狀況,it 的值就是 nullpost

SelectClause

咱們怎麼知道哪些事件能夠被 select 呢?其實全部可以被 select 的事件都是 SelectClauseN 類型,包括:學習

  • SelectClause0:對應事件沒有返回值,例如 join 沒有返回值,對應的 onJoin 就是這個類型,使用時 onJoin 的參數是一個無參函數:
    select<Unit> {
        job.onJoin { log("Join resumed!") }
    }
    複製代碼
  • SelectClause1:對應事件有返回值,前面的 onAwaitonReceive 都是此類狀況。
  • SelectClause2:對應事件有返回值,此外還須要額外的一個參數,例如 Channel.onSend 有兩個參數,第一個就是一個 Channel 數據類型的值,表示即將發送的值,第二個是發送成功時的回調:
    List(100) { element ->
        select<Unit> {
            channels.forEach { channel ->
                channel.onSend(element) { sentChannel -> log("sent on $sentChannel") }
            }
        }
    }
    複製代碼
    在消費者的消費效率較低時,數據能發給哪一個就發給哪一個進行處理,onSend 的第二個參數的參數是數據成功發送到的 Channel 對象。

所以若是你們想要確認掛起函數是否支持 select,只須要查看其是否存在對應的 SelectClauseN 便可。ui

小結

在協程當中,Select 的語義與 Java NIO 或者 Unix 的 IO 多路複用相似,它的存在使得咱們能夠輕鬆實現 1 拖 N,實現哪一個先來就處理哪一個。儘管 Select 和 Channel 比起標準庫的協程 API 已經更接近業務開發了,不過我的認爲它們仍屬於相對底層的 API 封裝,在實踐當中多數狀況下也可使用 Flow API 來解決。

而這個 Flow API,徹底就是響應式編程的協程版 API,咱們簡直能夠照着 RxJava 來學習它,因此咱們下一篇再見吧~~~


歡迎關注 Kotlin 中文社區!

中文官網:www.kotlincn.net/

中文官方博客:www.kotliner.cn/

公衆號:Kotlin

知乎專欄:Kotlin

CSDN:Kotlin中文社區

掘金:Kotlin中文社區

簡書:Kotlin中文社區

相關文章
相關標籤/搜索