笔记-Android中某些并发请求场景问题的处理办法

1,335 阅读4分钟

并发?什么鬼,这是后台开发才会考虑的事吧!亦或者重来没遇到过。

假如有如下场景,有一个列表排序按钮,在升序和降序之间切换,点击之后发送请求。那么如果用户快速频繁的点击按钮,会出现的结果是:排序的结果偶尔是错误的。注意“偶尔”两字,你没看错,是偶尔得到的排序结果才是错误的,其他时候是正确的。这种问题,才是磨人的小妖精。

那为什么偶尔会错误呢?实际上在这种情况之下,最后得到的结果不是“最后执行排序的结果”,而是“最后完成排序的结果”(注意理解下差别)。更具体点说就是如果最后一次发送的是升序(或降序)请求,那么在最后一次请求结束时,还有降序(或升序)请求未结束,那么就会引起这种错误。

模拟代码

//业务模块代码
class ConcurrencyVM(application: Application) : AMViewModel(application) {   
    //用一个字符串表示最后的结果 
    private val _sortedProducts = MutableLiveData<String>()    
    val sortedProducts: LiveData<String> = _sortedProducts  

    fun loadSortProducts(ascending: Boolean) {       
        viewModelScope.launch {            
            _sortedProducts.value = sortedProducts(ascending)        
        }    
    }            

    private suspend fun sortedProducts(ascending: Boolean): String {        
        delay(Random().nextInt(4) * 1000L)//模拟当前任务消耗的时间不同        
        return if (ascending) "当前是升序结果" else "当前是降序结果"    
    }
}

//用例代码,用一个文本显示排序结果,用一个CheckBox文本表示当前执行的状态
...
vm.sortedProducts.observe(this, Observer {     
    view.text.text = it
})
view.checkbox.setOnCheckedChangeListener { buttonView, isChecked ->     
    if(isChecked){        
        buttonView.text = "执行升序请求"        
        vm.loadSortProducts(true)    
    }else{        
        buttonView.text = "执行降序请求"        
        vm.loadSortProducts(false)    
}}
...

没事的看官可以试一试上面的代码,频繁的切换CheckBox的选中状态,然后看文本的结果是否与CheckBox的文本对应。大致出错的概率是25%,原因如下:

if(最后一次请求最后完成){
    //50% 这种情况没有问题
}else{ 
    if(最后完成的请求与最后发送的请求参数一致(同为升序或者降序)){
        //25% 这种情况,结果上也没有问题,因为最有完成的请求与最后发送的请求的目的是一样的
    }{
        //25% 这种情况出错,即最后完成的请求与最后发送的请求的目的不一致(即参数不一样,一个为升序一个为降序)
    }  
}

解决办法

1、标志位

即在发送请求之后,在方法中校验处理标志位,从而达到控制。

比如对于按钮的点击,在点击之后禁用按钮、也或者是类似isDoing等标志位。

这种办法在某些并发场景中通常有效,但是在有些场景中可能无效,比如在很短的时间内快速点击按钮(按钮抖动点击)、哪怕是用方法标志位时,在切换标志位值时已经发出了多个执行(类似于单例实现模式中双重检测+同步锁还是有可能出现问题的情况)。

2、队列

没错,排好队,一个一个来。

队列在并发请求问题中,通常是非常有效的,因为队列使得发送的请求顺序与处理顺序可以一致从而保证结果的正确性。

由于这种方法使得请求都按照顺序得到了执行,因此可能在某些场景中造成资源的浪费。比如频繁的刷新请求,所有刷新的执行过程和结果是一样的,在这种场景中用户仅仅是希望得到一次有效结果而已。所以可以在附带一些队列的其他处理策略,诸如同类型行为一并处理或取消入队等,具体策略视场景而定。

3、取消前一个任务

在开始下一个任务前,将前一个任务取消。

在前面举举例的排序场景中,当用户点击一次排序按钮,就意味着可以取消前一个排序请求了,毕竟前一个请求的结果已经失去意义了?

这种方式可能不适合全局单例使用,因为不相关的请求者不应该互相取消。

4、加入前一个任务

与第三个方法相反,如果新的请求可以重复使用已经存在的请求,对于已经在执行或者等待执行的任务来说,这样处理是一个非常不错的好办法。比如刷新场景,如果已经有刷新任务正在执行了,就没必要在执行一个新的刷新任务了。当然,这种方法对于前面的排序场景并不适用。

5、更多

确实还有更多的方式方法,比如前面方法的综合结合,队列+加入前一个任务、或者队列+策略(诸如同类型任务同时出队等)。

总的来说,没有全能的方案,使用的方法要根据具体的场景来选择和变化。