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


<<:  【Day28】Git 版本控制 - GitBook 简介

>>:  Youtube Reports API 教学 - 频道中出报表

JS 26 - 进阶版互动视窗!不只警告、确认和提示,还有导览功能!

大家好! 我们今天要实作能和使用者互动的视窗。 我们进入今天的主题吧! 互动视窗 如果要和使用者互动...

Day27 火堆实作 - 连接模组方块

在进到 " Shading " 之前,我们必须先调整 " Partic...

Day 22 来写一个简单e2e测试

今天我们来写一个简单的form来当作测试吧,首先我们刻出一个简单的画面 const App: FC ...

绝对路径及相对路径

上一篇提到图片汇入方式最重要环节就是src属性,相对路径及绝对路径使用方式 要注意相对路径档案及绝...

Day 0x17 UVa10252 Common Permutation

Virtual Judge ZeroJudge 题意 输入 a、b 两字串,输出皆为两者的子字串 ...