iT邦幫忙

2021 iThome 鐵人賽

DAY 28
0
Mobile Development

解鎖kotlin coroutine的各種姿勢-新手篇系列 第 28

day28 等一下啦,會壞掉的/// Coroutine併發操作的重複請求

沒有要開車,參賽規定有寫不能污言穢語,等我有空再去其他平台寫個開車系列的coroutine

這裡給個快轉,android開發者從1開始看,ktor從2開始看

如標題所訴,有時在客戶端會因為對方重複點擊按鈕,在結果還沒出來就發多一次請求,導致最後結果出錯,~~就壞掉了~~ 就有bug了

而實際上發生什麼事呢? 預期的工作順序應該是

  1. 發出排序請求
  2. 對資料排序
  3. 將結果回傳

但在多次請求的時候,將不是回傳排序結果,而是最後一個排序工作完成的結果,中間大概就是divide and conquer的過程一直被打亂,導致結果不如預期

而這其實跟排序的邏輯沒有關係,排序的邏輯是正確的,也和coroutine本身沒有關係,在任何多執行緒的程式,都有可能發生這個問題,而其實只是沒有妥善處理觸發機制

那它其實有四種解法

第一種,disable the button禁用按鈕


對的,最簡單也最方便的方法就是在過程中讓按鈕不能按,這種設計其實很常見,限購產品呀、無效登入呀、防止洗版等等,(當然前端後端都擋是最好的),而我們通常會設一個條件,讓按鈕變成可按

而且這個方法的優點不只解決我們的問題,測試寫起來也方便

viewModelScope.launch{
    _sortButtonsEnabled.value = false
    try{
        sortByAlphabet()
    } catch(e:Exception){
    
    } finally {
        _sortButtonsEnabled.value = true
    }
}

這邊用liveData示範,非常簡單卻又實際的解法

這邊提醒一個小細節,在launch預設是在main執行,這裡也充分利用這個特性,如果你切換了dispatcher,有可能disenable的速度感不上某些使用者的觸發

------嗶嗶,難易度分隔線,如果上述方法已經解決問題,可以有空了在了解其他解法,以ktor來說,直接來後面找答案吧,後端又沒按紐-----

由大大提供的gist,剩下的要搭配著看

第二種,取消前一個請求,同樣適用於後端的

有時我們在某個條件達成後,會執行某個動作,而我們永遠以最新的要求為主,所以會取消前面的請求

跟著大大寫起來就會是這樣

var controlledRunner = ControlledRunner<List<ProductListing>>()
...
suspend fun ...
{
    return controlledRunner.cancelPreviousThenRun {
        //someThing
    }
}

而這個cancelPreviousThenRun和ControlledRunner不是原生的,都在那個gist裡面,我這邊簡單介紹一下,為什麼他能確保取消前一個病執行最新的任務呢

在他的cancelPreviousThenRun裡面是這麼寫的

suspend fun cancelPreviousThenRun(block: suspend() -> T): T {
    // fast path: if we already know about an active task, just cancel it right away.
    activeTask.get()?.cancelAndJoin()

而這個activeTask是

private val activeTask = AtomicReference<Deferred<T>?>(null)

AtomicReference翻過來應該叫原子性引用,而這個原子性保證了在多個線程中修改,不會使結果不一致
直接開門,不然又要講很多 AtomicReference原子性引用

Important: This pattern is not well suited for use in global singletons, since unrelated callers shouldn’t cancel each other.

第三種,丟棄新的請求

這個方式會以舊的請求為主,畢竟如果工作一樣,何必再多做新的呢?
比如,對同個api發出5次請求,你明知每次都會拿到一樣的結果,何必讓客戶端做重工

var controlledRunner = ControlledRunner<List<ProductListing>>()
...
suspend fun ... {
    return controlledRunner.joinPreviousOrRun {
        //something
    }
}

那他是如何做到放棄新請求的

suspend fun joinPreviousOrRun(block: suspend () -> T): T {
    // fast path: if there's already an active task, just wait for it and return the result
    activeTask.get()?.let {
        return it.await()
    }
    ...
}

聽著困難,但其實大家應該都寫過了,就是如果activeTask存在,就return它

第四種,將任務照順序執行

 val singleRunner = SingleRunner()
 suspend fun ... {
     return singleRunner.afterPrevious {
         //someThing
     }
 }

如果你希望每個任務都會發生,但是要照請求的順序執行,大大的SingleRunner是以mutex去實作

mutex是一個鎖,文檔,你可以想像成水上樂園的滑水道,會有一個人在把關,等前一個人出了滑水道(任務結束),再讓下一個排隊的人進滑水道,代碼長這樣,mutex會在return時自動釋放

mutex.withLock {
    return block()
}

mutex有lock和unlock,而withLock幾本上就是取代

mutex.lock(); try { …… } finally { mutex.unlock() }

連結

必看

Coroutines real work

ConcurrencyHelpers.kt


上一篇
day27 coroutine和任務的愛情長跑,application和workManager
下一篇
day29 大量操作怎麼辦? 連volatile都救不了我QQ
系列文
解鎖kotlin coroutine的各種姿勢-新手篇30

尚未有邦友留言

立即登入留言