加工中心面板解锁

加工中心面板解锁▉▉▉【一电一 17154833762-】▉▉▉其实把若干异步任务串行化是最简单的解决办法,即前一个异步任务执行完毕后再执行下一个。但这样就无法利用多核性能,执行时间被拉长,此时的执行总时长 = 所有任务执行时长的和。

若允许任务并发,则执行总时长 = 执行时间最长任务的耗时。时间性能得以优化,但随之而来的一个复杂问题是:“如何等待多个异步结果”。

本文会介绍几种解决方案,并将它们运用到不同的业务场景,比对一下哪个方案适用于哪个场景。

等待并发网络请求

布尔值

假设有如下两个网络请求:

// 拉取新闻
fun fetchNews() {
    newsApi.fetchNews().enqueue(object : Callback> {
        override fun onFailure(call: Call>, t: Throwable) { ... }
        override fun onResponse(call: Call>, response: Response>) { ... }
    })
}
// 拉取广告
fun fetchAd() {
    newsApi.fetchAd().enqueue(object : Callback> {
        override fun onFailure(call: Call>, t: Throwable) { ... }
        override fun onResponse(call: Call>, response: Response>) { ... }
    })
}
复制代码

广告需要按一定规则插入到新闻列表中。

最简单的做法是,先请求新闻,待其返回后再请求广告。显然这会增加用户等待时间。而且会写出这样的代码:

// 拉取新闻
fun fetchNews() {
    newsApi.fetchNews().enqueue(object : Callback {
        override fun onFailure(call: Call, t: Throwable) { ... }
        override fun onResponse(call: Call, response: Response) {
                // 拉取广告
                newsApi.fetchAd().enqueue(object : Callback {
                    override fun onFailure(call: Call, t: Throwable) { ... }
                    override fun onResponse(call: Call, response: Response) { ... }
                })
        }
    })
}
复制代码

嵌套回调,若再加一个接口,回调层次就会再加一层,不能忍。 用户和程序员的体验都不好,得想办法解决。

第一个想到的方案是布尔值:

var isNewsDone = false
var isAdDone = false
var news = emptyList()
var ads = emptyList()
// 拉取新闻
fun fetchNews() {
    newsApi.fetchNews().enqueue(object : Callback> {
        override fun onFailure(call: Call>, t: Throwable) { 
            isNewsDone = true
            tryRefresh(news, ad)
        }
        override fun onResponse(call: Call>, response: Response>) { 
            isNewsDone = true
            news = response.body().result
            tryRefresh(news, ad)
        }
    })
}
// 拉取广告
fun fetchAd() {
    newsApi.fetchAd().enqueue(object : Callback> {
        override fun onFailure(call: Call>, t: Throwable) { 
            isAdDone = true
            tryRefresh(news, ad)
        }
        override fun onResponse(call: Call>, response: Response>) { 
            isAdDone = true
            ads = response.body().result
            tryRefresh(news, ad)
        }
    })
}
// 尝试刷新界面(只有当两个请求都返回时才刷新)
fun tryRefresh(news: List, ads: List) {
    if(isNewsDone && isAdDone){ //刷新界面 }
}
复制代码

设置两个布尔值分别对应两个请求是否返回,并且在每个请求返回时检测两个布尔值,若都为 true 则进行刷新界面。

网络库通常会将请求成功的回调抛到主线程执行,所以这里没有线程安全问题。但如果不是网络请求,而是后台任务,此时需要将布尔值声明为volatile保证其可见性,关于 volatile 更详细的解释可以点击面试题 | 徒手写一个非阻塞线程安全队列 ConcurrentLinkedQueue?。

这个方案能解决问题,但只适用于并发请求数量很少的请求,因为每个请求都要声明一个布尔值。而且每增加一个请求都要修改其余请求的代码,可维护性差。

CountdownLatch

更好的方案是CountDownLatch,它是java.util.concurrent包下的一个类,用来等待多个异步结果,用法如下:

val countdownLatch = CountDownLatch(2)//初始化,等待2个异步结果
var news = emptyList()
var ads = emptyList()
// 拉取新闻
fun fetchNews() {
    newsApi.fetchNews().enqueue(object : Callback> {
        override fun onFailure(call: Call>, t: Throwable) { 
            countdownLatch.countDown()
        }
        override fun onResponse(call: Call>, response: Response>) { 
            news = response.body().result
            countdownLatch.countDown()
        }
    })
}
// 拉取广告
fun fetchAd() {
    newsApi.fetchAd().enqueue(object : Callback> {
        override fun onFailure(call: Call>, t: Throwable) { 
            countdownLatch.countDown()
        }
        override fun onResponse(call: Call>, response: Response>) { 
            ads = response.body().result
            countdownLatch.countDown()
        }
    })
}
// countdownLatch 在新线程中等待
thread { 
    countdownLatch.await() // 阻塞线程等待两个请求返回
    liveData.postValue() // 抛数据到主线程刷刷新界面
}.start()
复制代码

CountDownLatch 在构造时需传入一个数量,它的语义可以理解为一个计数器。countDown() 将计数器减一,而 await() 会阻塞当前线程直到计数器为止 0 才被唤醒。

该计数器是一个 int 值,可能被多线程访问,为了保证线程安全,它被声明为 volatile,并且 countDown() 通过 CAS + 自旋的方式将其减一。

关于 CAS 自我介绍可以点击面试题 | 徒手写一个非阻塞线程安全队列 ConcurrentLinkedQueue?。

若新增一个接口,只需要将计数器的值加一,并在新接口返回时调用 countDown() 即可,可维护性陡增。

协程

Kotlin 是降低复杂度的大师,它对于这个问题的解决方案可以让代码看上去更简单。

在 Kotlin 的世界里异步操作应该被定义为suspend方法,retrofit 就支持这样的操作,比如:

interface NewsApi {
    @GET("/xxx")
    suspend fun fetchNews(): News
    @GET("/xxx")
    suspend fun fetchAd(): Ad
}
发表评论
留言与评论(共有 0 条评论) “”
   
验证码:

相关文章

推荐文章