开发人员始终面临着需要解决的问题——如何防止应用程序被阻塞。 开发桌面应用,移动应用,甚至服务端应用程序时,希望避免让用户等待或阻碍应用程序扩展。
以下会介绍实现异步编程的不同方式 ,包括:
- 线程
- 回调
Futures ,Promises 等等- 响应式扩展
- 协程
1 线程
到目前为止,线程可能是最常见的避免应用程序阻塞的方法:
fun postItem(item: Item) {
val token = preparePost()
val post = submitPost(token, item)
processPost(post)
}
fun preparePost(): Token {
return token
}
假设在上面的代码中,preparePost 是一个长时间运行的线程,因此会阻塞用户界面。我们可以做的是在一个单独的线程中启动它。这样就可以允许我们避免阻塞UI 。这是一种非常常见的技术,但有一系列缺点:
- 线程切换十分浪费资源
- 可被启动的线程数受底层操作系统的限制,无能无限启动。在服务器端应用程序中,这可能会导致严重的瓶颈
- 在一些平台中,比如
JavaScript 并不支持线程 - 线程不容易使用。线程的
Debug ,避免竞争条件是在多线程编程中遇到的常见问题
2 回调
使用回调,将一个函数作为参数传递给另一个函数,并在处理完成后调用此函数:
fun postItem(item: Item) {
preparePostAsync { token ->
submitPostAsync(token, item) { post ->
processPost(post)
}
}
}
fun preparePostAsync(callback: (Token) -> Unit) {
}
原则上这感觉就像一个更优雅的解决方案,但又有几个问题:
- 回调嵌套的难度。通常被用作回调的函数,经常最终需要会调自己,这导致出现一系列难以理解的回调嵌套。该模式通常被称为标题圣诞树(大括号代表树的分支)。
- 错误处理很复杂。嵌套模型使错误处理变得更加复杂。
回调在诸如JavaScript 之类的事件循环体系结构中非常常见,但是通常会使用其他方法,例如promises 或响应式扩展。
3 Futures ,Promises 等等
futures 或promises 的原理是当发起调用的时候,将会在某些时候返回一个Promise 类型的可被操作的对象:
fun postItem(item: Item) {
preparePostAsync()
.thenCompose { token ->
submitPostAsync(token, item)
}
.thenAccept { post ->
processPost(post)
}
}
fun preparePostAsync(): Promise<Token> {
return promise
}
这种方法需要对编程方式进行一系列更改,尤其是
- 不同的编程模型。与回调类似,编程模型从自上而下的命令式方法转变为具有链式调用的组合模型。传统的编程结构例如循环,异常处理,等等。通常在此模型中不再有效
- 不同的
API 。通常这需要学习完整的新API 诸如 thenCompose 或 thenAccept ,这也可能因平台而异 - 具体的返回值类型。返回类型不是需要的实际数据,而是返回一个新类型
Promise - 异常处理会很复杂。误处理变得更加复杂
4 响应式扩展
Rx 的理念是“可观察流”,将数据视为流(无限量的数据),并且可以观察到这些流。 实际上,Rx 很简单, Observer Pattern (观察者模式) 带有一系列扩展,允许我们对数据进行操作。著名的表述是:“一切都是流,并且它是可被观察的”
observer [?b?z??rv?r] 观察者,目击者;观察家,评论员;(会议等的)观察员 pattern [?p?t?rn] 模式;图案;样品
在方法上它与Futures 非常相似,但是开发者可以将Future 视为一个离散元素,而Rx 返回一个流。Rx 的一个好处是,它被移植到这么多平台,通常我们可以找到一致的API 体验,无论我们使用 C# 、Java 、JavaScript ,还是Rx 可用的任何其他语言。
此外,Rx 确实引入了一种更好的错误处理方法。
5 协程
Kotlin 编写异步代码的方式是使用协程,这是一种计算可被挂起的理念:即一种函数可以在某个时刻暂停执行并稍后恢复。
对于开发人员来说,协程的一个好处是:编写非阻塞代码与编写阻塞代码基本相同,编程模型本身并没有真正改变。
以下面的代码为例:
fun postItem(item: Item) {
launch {
val token = preparePost()
val post = submitPost(token, item)
processPost(post)
}
}
suspend fun preparePost(): Token {
return suspendCoroutine { }
}
launch[l??nt?] 发动,发起; suspend [s??spend] 暂停,中止;使暂停使用(或生效)
此代码将启动长时间运行的操作,而不会阻塞主线程。preparePost 就是所谓的 可挂起的函数 ,它含有suspend 前缀,该函数将被执行、暂停执行以及在某个时间点恢复。
- 与普通函数相比,唯一的不同是它被添加了
suspend 修饰符 - 编写这段代码代码就好像在编写同步代码,自上而下,不需要任何特殊语法,除了使用一个名为
launch 的函数,它实质上启动了该协程 - 编程模型和
API 保持不变。可以继续使用循环,异常处理等,而且不需要学习一整套新的API 。 - 与平台无关。无论我们是面向
JVM ,JavaScript 还是其他任何平台,我们编写的代码都是相同的,编译器负责将其适应每个平台
协程并不是一个新的概念,它已经存在了几十年,在Go 等其他一些编程语言中很受欢迎。但重要的是要注意是它们在Kotlin 中实现的方式,大部分功能都委托给了库。事实上,除了suspend 关键字,没有任何其他关键字被添加到Kotlin 中,这也是与其他语言的不同之处,例如C# 将async 以及 await 作为语法的一部分,而在Kotlin 中,它们都只是库函数。
参考
https://www.kotlincn.net/docs/tutorials/coroutines/async-programming.html
|