异步编程浅谈

606 阅读5分钟

异步编程浅谈

怎样防止程序被阻塞,是开发者一直以来面临的一个问题,我们的程序总是被这样那样的代码所阻塞,不管是开发桌面程序、移动端还是服务端程序。

阻断就意味着用户需要等待,直接影响应用程序的用户体验。

目前有几种方式来进行解决该问题,大致有一下几种方式

  • 线程
  • 回调
  • 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关键字就实现该协程的使用(当然还有其他类库的支持),还是非常简洁且令人期待的。