java程序员的kotlin课(N+2):suspending函数执行编排

英文原文

执行编排指的是对异步函数的执行顺序进行控制,举个简单的例子:
需要调用a、b两个函数,从高效的角度讲a、b并行调用有一定是最佳的,但是如果b函数的输入依赖a函数的输出,那就只能做串行调用。
针对a、b两个函数执行顺序的控制,即本节要讲的执行编排,原文中的 Composing Suspending Functions

默认串行执行

假设我们有两个suspending的函数,可能是远程调用(rpc)。我们假装这两个函数很有用,虽然这两个函数只是简单的delay了一下.

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L) // pretend we are doing something useful here
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L) // pretend we are doing something useful here, too
    return 29
}

我们如何让这两个函数串行的执行呢?依次执行doSomethingUsefulOne和doSomethingUsefulTwo,然后计算两个函数结果的和,请看下面的代码(为了计算两个函数总的执行时间,此处利用了measureTimeMillis函数)

val time = measureTimeMillis {
    val one = doSomethingUsefulOne()
    val two = doSomethingUsefulTwo()
    println("The answer is ${one + two}")
}
println("Completed in $time ms")

执行结果如下:

The answer is 42
Completed in 2017 ms

并行执行异步操作

如果两次函数调用之间没有有起来关系,我们就可以并行的执行两个函数了,这样执行起来总的耗时会减少。
从概念上讲,async和launch是相似的。他们都可以启动一个协程,区别在于launch返回一个job对象,job对象不携带任何结果相关的信息。而async返回一个Deferred类型的对象,和future类似,不过这个是在协程背景下的,可以通过.await()函数来获得结果。而且Deferred也是一个job,一样可以用来cancel协程。

val time = measureTimeMillis {
    val one = async { doSomethingUsefulOne() }
    val two = async { doSomethingUsefulTwo() }
    println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")

输出结果如下:

The answer is 42
Completed in 1017 ms

执行速度有两倍的提升,因为两个协程是并行执行的。谨记:利用协程的并行永远是显式的

延迟启动异步动作

作为一个可选项,在使用async关键字启动协程的时候,可以传入参数CoroutineStart.LAZY。在这种模式下,协程启动的时机:

  • await函数被调用时
  • start函数被调用时
val time = measureTimeMillis {
    val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() }
    val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() }
    // some computation
    one.start() // start the first one
    two.start() // start the second one
    println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")

结果如下:

The answer is 42
Completed in 1017 ms

上面的例子中,两个协程并没有像之前的例子一样立刻启动,启动的控制权交给了编码者,编码者可以通过start函数或者await函数进行启动。
但是需要注意的是,如果使用await来触发协程的启动,上面的例子会变为串行执行,因为aswit函数会等待协程执行完毕才向下执行接下来的动作。

异步风格的函数

我们可以通过 async coroutine builder 和显式的 GlobalScope来把 doSomethingUsefulOne函数和doSomethingUsefulTwo函数声名为异步函数:

// The result type of somethingUsefulOneAsync is Deferred<Int>
fun somethingUsefulOneAsync() = GlobalScope.async {
    doSomethingUsefulOne()
}

// The result type of somethingUsefulTwoAsync is Deferred<Int>
fun somethingUsefulTwoAsync() = GlobalScope.async {
    doSomethingUsefulTwo()
}

请注意,上面的xxxAsync函数并不是suspending函数,他们可以在任何地方被调用。下面的代码是用来调用上面两个函数的代码:

// note that we don't have `runBlocking` to the right of `main` in this example
fun main() {
    val time = measureTimeMillis {
        // we can initiate async actions outside of a coroutine
        val one = somethingUsefulOneAsync()
        val two = somethingUsefulTwoAsync()
        // but waiting for a result must involve either suspending or blocking.
        // here we use `runBlocking { ... }` to block the main thread while waiting for the result
        runBlocking {
            println("The answer is ${one.await() + two.await()}")
        }
    }
    println("Completed in $time ms")
}

上面这种编码风格仅用来展示,因为在其他语言中,这是一种很流行的编码方式,但是在kotlin中这种编码风格是强烈不推荐的。
还记得前几章有讲过一个词结构化并发或者英文原版structured concurrency,啥意思呢,就是当父协程取消、异常、关闭时,需要保证子协程都有被正确的取消,避免资源泄漏。
那么上面的代码呢?如果val one = somethingUsefulOneAsync()这一行到one.await()这一行发生任何错误,会怎样?主协程停止了,报错了,但是子协程会一直执行。

通过async做结构化异步编程

所以上面的例子,还是需要进行结构化的编码风格来做:

fun main() = runBlocking {
  var a = async { doSomethingUsefulOne() }
  var b = async { doSomethingUsefulTwo() }
  println(a.await() + b.await())
}

suspend fun doSomethingUsefulOne(): Int {
  delay(1024)
  return 23
}

suspend fun doSomethingUsefulTwo(): Int {
  delay(800)
  return 35
}

这种编码风格,如果main中发生任何错误,所有的子协程都将被取消。

fun main() = runBlocking {
  var a = async { doSomethingUsefulOne() }
  var b = async { doSomethingUsefulTwo() }
  println(a.await() + b.await())
}

suspend fun doSomethingUsefulOne(): Int {
  delay(200)
  throw RuntimeException()
  return 23
}

suspend fun doSomethingUsefulTwo(): Int {
  try {
    //永远都不会被执行
    delay(Long.MAX_VALUE)
    return 35
  } finally {
    println("cancel.")
  }
}

读者可以自己试一下上面的代码。

系列文章快速导航:
java程序员的kotlin课(一):环境搭建
java程序员的kotlin课(N):coroutines基础
java程序员的kotlin课(N+1):coroutines 取消和超时
java程序员的kotlin课(N+2):suspending函数执行编排

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,744评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,505评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,105评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,242评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,269评论 6 389
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,215评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,096评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,939评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,354评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,573评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,745评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,448评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,048评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,683评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,838评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,776评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,652评论 2 354