java程序员的kotlin课

java程序员的kotlin课(N+2):suspending函

2020-01-24  本文已影响0人  青_雉

英文原文

执行编排指的是对异步函数的执行顺序进行控制,举个简单的例子:
需要调用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。在这种模式下,协程启动的时机:

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函数执行编排

上一篇 下一篇

猜你喜欢

热点阅读