这里将介绍不同的异步编程实现。
作为程序员,我们都面临着一个问题,就是如何不让我们的程序阻塞。无论我们是桌面开发,移动开发,甚至服务端开发。
有很多不同的实现来解决这个问题,包括:
-Threading
-Callbacks
-Futures, Promises
-Reactive Extensions
-Coroutines
我们先简明的看下前四种实现方式。
到目前为止,线程是最为程序员所知的一种避免阻塞应用的实现。
fun postItem(item: Item) {
val token = preparePost()
val post = submitPost(token, item)
processPost(post)
}
fun preparePost(): Token {
// makes a request and consequently blocks the main thread
return token
}
我们假设 preparePost
方法是一个很耗时的方法,这样的话,它将会阻塞用户交互。我们可以把这个方法放在一个单独的线程中,这样就避免了阻塞UI线程。这是一个非常常见的技术,但是它有一系列的缺点:
-线程很耗资源,它们要切换上下文
-线程数量有限,操作系统能启动的线程数量是有限的,对于服务端来说,这是一个很大的瓶颈
-线程并不总是可用的,比如JavaScript就不支持线程
-编写好的代码并不容易,容易出现各种竞争条件,会陷入并发编程苦海
简单来说,就是把函数作为参数传递给另一个函数,一旦本身函数执行完成,就执行传参的函数。
fun postItem(item: Item) {
preparePostAsync { token ->
submitPostAsync(token, item) { post ->
processPost(post)
}
}
}
fun preparePostAsync(callback: (Token) -> Unit) {
// make request and return immediately
// arrange callback to be invoked later
}
这个看起来相对优雅的解决了问题,但是同样有几个问题:
-对于多重嵌套回调,这将是一个非常难理解的方式
-错误处理变得很复杂,对于多重嵌套,错误的传递和处理变得很复杂
Callbacks在事件循环结构的语言中比较常见,比如JavaScript,但更多的码农倾向于其它的解决方案,比如promises或者reactive extensions.
Futures,Promises,不同的平台或语言有不同的叫法,它是承若将在某个点返回一个叫Promise的对象,简略操作如下所示:
fun postItem(item: Item) {
preparePostAsync()
.thenCompose { token ->
submitPostAsync(token, item)
}
.thenAccept { post ->
processPost(post)
}
}
fun preparePostAsync(): Promise {
// makes request an returns a promise that is completed later
return promise
}
这个方式对于我们来说,有一点挑战,比如说:
-不同的编程模型。和callback类似,从上而下的链式回调。传统的编程结构,比如循环,错误处理都不在有效
-不同的平台有不同的API
-具体的返回类型,返回类型并不是我们实际的数据结构
-错误处理变得复杂。错误的传递处理并不清晰明确。
响应式扩展(Rx)被Eik Meijer引入C#,虽然它确实存在于.NET平台,但直到被Netflix移植到Java平台,才逐渐开始成为主流。从这起,各个平台实现自己的响应式扩展,包括JavaScript(RxJS)。
Rx的背后思想是把数据当作流来处理,并且该流可以被观测。实际上,Rx只是观察者模式,带有一系列的扩展来操作数据。
Rx和Futures很类似,但不同的是,我们可以认为Future返回一个直接的元素,而Rx返回流。另一方面,它提供了一中新的编程模型,就像所宣传的口号那样:
everything is a stream, and it’s observable
这意味着有不同的解决方法,并且提供了不同的思路去写异步代码。相比于Futures不同的是,Rx被移植到多个平台,我们有着一致的API体验。包括C#,Java,JavaScript,或者其它实现了Rx的语言。
此外,Rx引入了更友好的错误处理方式。
Kotlin异步编程的方式是使用coroutines,这是一种可挂起的执行方式,它可以在某一点挂起,之后从这点恢复继续执行。
对于码农来说,使用协程方法编写异步代码和编写同步代码没有什么不同,这中编码模型并不存在什么挑战。
举个栗子:
fun postItem(item: Item) {
launch {
val token = preparePost()
val post = submitPost(token, item)
processPost(post)
}
}
suspend fun preparePost(): Token {
// makes a request and suspends the coroutine
return suspendCoroutine { /* ... */ }
}
这段代码将会执行耗时工作,但并不阻塞主线程。带有 suspend
修饰的 preparePost
方法就是被成为可挂起函数。就像上面描述的那样,它可以在某个点挂起,然后从该点恢复执行。
-函数签名保持了一致,仅增加了 suspend
修饰符。返回值也是我们想要的类型
-编写代码和我们以前一样,并不需要特殊的语法
-编程模型和API保持了一样,我们可以继续使用循环,异常处理,不需要学习新的api集
-它是平台无关的,无论是运行在JVM,JavaScript平台,还是其它,我们写法都是一样的,编译器负责适配各个平台。
Coroutines并不是Kotlin发明的新概念,它们已经存在了几十年了,并且在其它语言非常流行,比如Go语言。需要注意的是,虽然Kotlin实现了Coroutines,但大多数功能都是在库函数里边。事实上,除了 suspend
关键词,没有其它的关键词被引入到语言中。这个和C#有着较大的不同,C#引入 async
和 await
做为语法的一部分。而对于Kotlin,这些只是库函数。