异步编程浅谈
怎样防止程序被阻塞,是开发者一直以来面临的一个问题,我们的程序总是被这样那样的代码所阻塞,不管是开发桌面程序、移动端还是服务端程序。
阻断就意味着用户需要等待,直接影响应用程序的用户体验。
目前有几种方式来进行解决该问题,大致有一下几种方式
- 线程
- 回调
- Future和Pormise
- Reactive扩展
- 协程(Coroutines)
线程
线程可能是我们最耳熟能详的处理阻塞的方法
fun postItem(item:Item){
val token = preparePost()
val post = submitPost(token,item)
processPost(post)
}
fun preparePost():Token{
//耗时后台操作,阻塞主线程
return token
}
以上代码会阻塞主线程,可能会影响用户界面相应,当然,我们可以将该耗时操作放在线程里,但是有几个弊端
- 线程不是廉价的,线程的创建以及上下文切换,都是有成本的
- 线程并非可以无限创建的。线程的创建受限于底层操作系统,在服务器端应用,这会是开发的最大瓶颈
- 线程也并非一直都有效,因为有些平台,比如说JavaScript就不支持线程
- 线程操作也并非简单的,调试线程特别是多线程中的避免竞争都并非易事
回调
利用回调处理问题的思路是,将函数作为一个参数传递给另一个函数,一旦该操作完成,那么接着调用执行另一个操作
fun postItem(item:Item){
preparePostAsync{token->
submitPostAsync(token,item){
processPost(post)
}
}
}
fun preparePostAsync(callback:(Token)->Unit){
//耗时操作执行后,立即调用回调函数
}
该回调的方式看上去智能了很多,但是还是有一些问题
- 嵌套回调让人不厌其烦,著名的回调地狱(Callback Hell可以了解下),一个个括号,让人想起来Dart里的括号
- 错误处理复杂化
回调在event-loop的架构里非常普遍,比较知名的是JavaScript,但即便那样,大家也逐渐转向了Promise或者Reactive extensions(响应式扩展)
Future,Prosmises及其他
futures或者promises背后贯穿的思想是,当我们调用时,就认定该调用执行会有一个返回,该返回我们称之为promise,该promise能够继续进行后续的操作
fun postItem(item:Item){
preparePostAsync().
thenCompose{token ->
submitPostAsync(token,item)
}.thenAccept{ post->
processPost(post)
}
}
fun preparePostAsync():Pormise<Token>{
//耗时处理,处理完毕后返回promise
return promise
}
该方案需要一系列的编程上的改变,特别是如下几点
- 不同的编程模型。和回调类似,编程模式因为链式调用从一种由上而下的命令式转变为组合式模型,在这种模型里,传统的循环和异常处理等都将不再有效
- 不同的API。通常情况下,需要学习新的诸如thenCompose和thenAccept等的新接口
- 需指定返回类型。返回类型从我们需要的实际数据类型转变为一种新类型Promise
- 复杂化的错误处理。错误信息的传递变得不再直观
Reactive Extensions 响应式扩展
Rx首先在C#中出现,后来被网飞移植到Java平台,就是RxJava,自此,各种平台的移植纷纷开始,包括RxJs。
Rx背后的思想是将数据认定为流,而且这种流可以被观测。和Futures有点类似,只不过Futures返回的是分散的元素,但是Rx返回的是连续的流。万物皆是流,是流即可观。(everything is a stream, and it's observable)。相比Future的优点之一是,由于平台的迁移实现,我们可以使用统一的API接口不管我们用的是哪种语言。
另外,Rx对异常的处理也相对友好。
协程 Coroutines
kotlin计划使用协程来处理异步代码,协程的核心思想是可挂起计算,就是说函数可以在某个时刻挂起它的执行,然后在接下来某个时刻再恢复执行。
对开发者来说,协程的一大好处是,针对异步代码的编写开发和编写阻塞代码没有太大区别,编程的模式没有发生实质性变化
fun postItem(item:Item){
launch{
val token = preparePost()
val post = submitPost(token,item)
processPost(post)
}
}
suspend fun preparePost():Token{
//耗时操作,挂起协程
return suspendCoroutine{}
}
preparePost函数为挂起函数,使用关键字suspend来标识,意味着该函数可能会适时的执行、暂停、恢复执行
- 函数签名和普通函数没有差异,函数的唯一差异就是suspend关键字的添加,返回类型是我们想要的返回类型
- 代码和同步方式编写代码没有差异,还是从顶向下,且没有额外的特殊语法,仅仅用了一个launch语法
- 编程模式和API仍旧没有特殊变化,我们仍然可以继续使用循环、异常处理等,不需要学习新的API,学习成本极低
- 平台独立。目标平台不管是JVM、JavaScript还是其他的平台,编码均一致,编译器已经将所有差异化处理完毕
协程并非新概念,更不会是Kotlin新创的,Go语言中的协程已经存在很久了。但在Kotlin里仅仅增加了一个suspent关键字就实现该协程的使用(当然还有其他类库的支持),还是非常简洁且令人期待的。