加工中心面板解锁▉▉▉【一电一 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,它是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 条评论) “” |