Kotlin 协程实践(2)之 异步和Callback地狱

发布于: 2020 年 05 月 19 日
Kotlin 协程实践(2)之 异步和Callback地狱

Kotlin 协程实践系列文章自Roman Elizarov在KotlinConf 2018关于Kotlin协程演讲和笔者构建网络爬虫服务实践过程中的一些总结而来。

kotlin协程实现了同步非阻塞编程模式,目的之一就是让程序逻辑不阻塞当前线程,从而提高吞吐量和线程利用率。在开始之前我们还需要明白几个重要的概念。

同步、异步 & 阻塞、非阻塞

这几个概念,在刚开始参加工作的头几年里,一直没搞明白,感觉非常烧脑。网上也有很多文章通过举例子的方式来解释,但终究有些牵强。工作多年后,在对于操作系统、JVM、编程范式有了进一步理解后,我想用程序员的思维视角来解释一下。

至于这和协程有什么关系,我们留着后面讨论。

同步、异步:

这个需要站在编程语言的层面来看它们的差别。从API形式上来看,同步和异步的区别在于:

  • 同步:函数直接返回结果

  • 异步:调用函数时同时传递一个Callback,或者函数返回的一个Feature

阻塞、非阻塞:

一般情况都是由于IO操作导致阻塞和非阻塞。你可以站在程序或者线程的角度看待这个问题,但是站在程序的立场,很难解释非阻塞这个词汇,所以我们站在线程这个角度来看。

  • 阻塞:我是一个线程,当程序进行某一个调用时,我被阻塞(绑架)了,我不能去干其他事情,只能等待

  • 非阻塞 :我是一个线程,当程序进行某一个IO调用时,我知道他需要一些时间,所以我先去执行其他程序

显然非阻塞要能实现,程序必须是复杂的,必须需要运行时环境提供支持。至于运行时环境如何高效实现非阻塞,则必须和操作系统相互配合。(Java的NIO、linux的select、poll、epoll了解一下)

此刻记在你脑子里的是,是否阻塞是底层系统在IO调用时给你的能力,同步和异步只是API的形式。

从编程难以程度上来看,同步比异步要简单很多,也和人类思维过程比较接近。从系统性能角度来看,非阻塞效率相当高,在互联网高并发场景下,这尤为重要。

  1. 一般来讲阻塞和同步是配对出现。

val json = getDataFromUrl(url)
// 在漫长的等待后,执行第二条语句
println(json)

  1. 非阻塞和异步是配对出现。

//初始化异步http客户端
val httpAsyncClient = ...
// 该调用立刻返回
httpAsyncClient.execute(HttpGet("https://github.com/mayabot/mynlp"), object : FutureCallback<HttpResponse> {
override fun cancelled() {
TODO("Not yet implemented")
}
override fun completed(result: HttpResponse?) {
TODO("Not yet implemented")
}
override fun failed(ex: Exception?) {
TODO("Not yet implemented")
}
} )
// 打印语句无需等待,立刻执行
println("send call")

  1. 异步阻这个组合属于编程难度大、效率低下,吃力不讨好,我们放弃讨论

  1. 同步非阻塞

这个组合非常棒,编程难度低,执行效率高,非常难得。

Kotin协程就实现了这种组合,也就解决了异步编程痛点。

异步编程&Callback地狱

让我们看一个玩具级演示例子:

这个一个网络应用,要向服务器创建一个Post,必须先获得一个token令牌。

fun requestToken() {
// 发起阻塞的网络请求,等待返回Token对象
// 执行Thread被阻塞
return token // return result when received
}
fun createPost(token: Token,item: Item): Post {
// send item to the server and wait
// 执行Thread被阻塞
return post // return result when post
}
fun processPost(post: Post){
// 对Post对象进行一些处理,比如更新UI
}
// 整合起来
fun postItem(item: Item){
val token = requestToken()
val post = createPost(token,item)
processPost(post)
}

程序很简单对吧,这就是同步编程的优点,程序简单易读,但遗憾的是,Thread被阻塞了。

JVM的线程数量是有限的,创建100线程不是什么难事,但是创建10 000个呢?一个线程大概会占2M内存,1万个线程,就会占用20G,所以在有限的线程下,同步阻塞编程模型的吞度量会很快达到极限。

那么callback就出场来拯救这一切。nodejs为什么能处理高并发,就是得益于高性能的event loop和callback机制。

让我们看看Callback的代码形式

fun requestTokenAsync(callback: (Token)->Unit){
// 发起异步IO请求,当有返回结果时callback会被调用(用异步httpclient很容易实现)
// 函数立刻返回
}
fun createPostAsync(token: Token,item: Item,callback: (Post)->Unit) {
// 发送item和token到服务器,当返回Post时,callback被调用
// 函数立刻返回
}
fun processPost(post: Post){
// 对Post对象进行一些处理,比如更新UI
}
// 整合起来
fun postItem(item: Item){
requestTokenAsync{ token->
createPostAsync(token,item){ post->
processPost(post)
}
}
}

postItem里面的多层callback函数嵌套,就是callback地狱了。

也行你会想这种嵌套没那么可怕?no,no,no!

实际代码中轻松嵌套几十层,看的头皮发麻。然后上面只是描述了一个顺序结构,a->b->c。但是如果存在条件分支、异常处理、循环、错误重试等等逻辑呢。虽然后来RxJava等框架尝试改善异步编程的困境,大概思路把嵌套结构改为了chain结构,但是依然复杂且程序接近非人类可读。

nodejs后来通过promise对象与async/await关键字来解决callback地狱问题

让我们看看kotlin的协程版本

suspend fun requestToken() {
// 发起阻塞的网络请求,等待返回Token对象
// 执行Thread被阻塞
return token // return result when received
}
suspend fun createPost(token: Token,item: Item): Post {
// send item to the server and wait
// 执行Thread被阻塞
return post // return result when post
}
fun processPost(post: Post){
// 对Post对象进行一些处理,比如更新UI
}
// 整合起来
suspend fun postItem(item: Item){
(挂起点) val token = requestToken()
(挂起点) val post = createPost(token,item)
processPost(post)
}

和正常程序比,只是多了多了suspend关键字。其本质是kotlin在编译时,在suspend函数的参数签名,自动加入callback。

kotlin协程的实现方式,只引入了suspend一个关键字,剩余的实现是通过协程库来补充实现的。这样的好处在于通过升级库就能提升协程的功能,而不需要频繁求助于编译器。

当然编译器在处理suspend关键字时聪明得多,能处理任意复杂的程序逻辑,这样你可以继续利用已经掌握的传统编程里面的try\cache、while或者filter\map\forEach\repeat\filter等函数。

总而言之,就是kotlin协程帮你用阻塞的编程方式,实现异步编程。

小结

好了,希望我把这阻塞、异步等这些概念讲清楚了。

我当初学习使用kotlin协程,一开始也有很多困惑点。放心这些后面我们一一解释。但是要想能用好协程,得了解来龙去脉,了解为什么要设计协程这个抽象概念。

协程就是解决callback地狱,用阻塞编程方式实现异步程序,提高系统性能。

下一篇我们继续介绍suspend函数。

上一篇:Kotlin 协程实践(1)之进程、线程、协程

发布于: 2020 年 05 月 19 日 阅读数: 580
用户头像

陈吉米

关注

奥卡姆剃刀道理很简单,实践起来很难 2017.11.30 加入

jimichan@gmail.com https://github.com/mayabot

评论 (1 条评论)

发布
用户头像
感谢分享,InfoQ首页推荐这篇啦。
2020 年 05 月 20 日 21:55
回复
这么晚还在努力工作哈👍
2020 年 05 月 21 日 00:07
回复
感觉并没有被推🤩,是不是漏了还是看错了,🙃
2020 年 05 月 21 日 10:31
回复
推了,在5月19日,官网往下拉下,被新的刷下去了。回复没办法截图。
2020 年 05 月 21 日 12:24
回复
没有更多了
Kotlin 协程实践(2)之 异步和Callback地狱