0%

第1章:异步程序设计介绍

1.2 异步程序设计的关键问题

1.2.1 结果传递

不同于同步调用,异步调用是立即返回的,因此被调用方的逻辑通常存在2种情形:

  • 结果尚未就绪:进入任务执行的状态,等结果就绪后通过回调传递给调用方

  • 结果已经就绪,可以立即提供结果

用图片获取作为例子,上述2种情况代码示意如下所示(这是我个人觉得理解Kotlin协程实现最巧妙的例子了,其实后面的本质就是这个):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 方法实现
fun asyncBitmap(url: String, callback: (Bitmap) -> Unit): Bitmap {
val bitmap = asyncBitmap(url) {
show(it) //...② 异步请求
}

if (bitmap != null) {
show(bitmap) //... ① 直接返回了
} return when (val bitmap = Cache.get(url)) {
null -> {
thread {
download(url)
.also { Cache.put(url, it) }
.also(callback)
}
}

else -> bitmap
}
}


//使用方法,如果有同步返回,就使用同步返回,否则依赖异步回调
@JvmStatic
fun main(args: Array<String>) {
val bitmap = asyncBitmap(url) {
show(it) //...② 异步请求
}

if (bitmap != null) {
show(bitmap) //... ① 直接返回了
}
}

Kotlin 协程的挂起函数 (suspend function) 本质上就是采取了这个异步返回值的设计思路

1.2.2 异常处理

我们希望异步的任务在 suspend 的时候执行,执行完成后 resume 回来,之后再同步判断是否发生了异常。这样逻辑就会简单很多。异步逻辑同步化也是 Kotlin 协程要解决的问题。

1.2.4 复杂分支

为同步操作添加分支甚至循环操作是很容易的,比如获取图片是同步的情况下,获取多个图片:

1
val bitmap = urls.map { syncBitmap(it) }

在上述情况下我们甚至还能方便地通过一个 try-catch 捕捉所有异常。而当获取图片的操作是异步的情况时,就变得复杂,还需要用到一些同步工具来辅助:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
val countDownLatch = CountDownLatch(urls.size)

val map = urls.map { it to EMPTY_BITMAP }
.toMap(ConcurrentHashMap<String, Bitmap>())

urls.map { url ->
asyncBitmap(url, onSuccess = {
map[url] = it
countDownLatch.countDown() //...②
}, onError = {
showError(it)
countDownLatch.countDown()//...③
})
}

countDownLatch.await() //...①

//获取所有的图片 bitmap
val bitmaps = map.values

这里还需要借助 CountDownLatch 门闩 来实现,因此在 ① 处会阻塞当前线程,直到所有回调的 ② 或 ③ 位置执行之后,才会执行获取所有图片的操作:val bitmaps = map.values ,总之,这个过程还是挺复杂的。

EMPTY_BITMAP 是一个空的 Bitmap 对象,为什么要这么做呢?因为 ConcurrentHashMap 不允许 value 为空!

1.3 常见异步程序设计思路

1.3.1 Future

但是 Future.get() 方法会造成当前调用阻塞。

1.3.2 CompletableFuture

它的 get() 方法无需阻塞,异步调用不阻塞主流程调用但是结果脱离了主调用流程(需要回调获取)

1.3.3 JavaScript 的 Promise——async/await

比较完好地实现了需求,他们 async/await 的语义稍微不同。

1.3.5 Kotlin协程

Kotlin 协程是为异步程序设计而生的。

有人称协程只是“一个线程框架”,认为协程就是用来切换线程的,显然有点“一叶障目不见泰山”了

Kotlin 协程用一个 suspend 关键字,包含了异步调用回调 两层含义。我们知道,所有异步回调对于当前调用流程只是一个挂起点。在这个挂起点可以做的事情非常多:既可以做异步回调,还可以添加调度器来处理线程切换,还可以作为协程取消响应的位置。

看一个 Kotlin 处理异步调用的例子:

1
2
3
4
5
6
7
8
9
10
suspend fun bitmapSuspendable(url: String): Bitmap =
suspendCoroutine<Bitmap> { continuation ->
thread {
try {
continuation.resume(download(url))
} catch (e: Exception) {
continuation.resumeWithException(e)
}
}
}

suspend 修饰函数,意味着支持同步化的异步调用。上述代码中,

  • continuation.resume(download(url)) 将正常的结果返回

  • continuation.resumeWithException(e) 则是将异常返回,在调用 bitmapSuspendable() 方法时,如果产生异常会把这个异常抛出

效果如下:

1
2
3
4
5
6
7
8
suspend fun main() {
try {
val bitmap = bitmapSuspendable("xxxx")
//todo 正常图片处理
} catch (e: Exception) {
//todo 异常处理
}
}

从 1.3.0 开始,开始支持 suspend fun main 作为函数入口了

谢谢你的鼓励