在前一篇文章我们知道 suspend 函式必须要在 Coroutine scope 里面才能执行,本篇文章我们来了解一下两个 Coroutine Builder : launch()
、 async()
如果一个 Coroutine 的回传值没有回传值,也就是回传 Unit
时,就可以使用 launch()
来建立一个 Coroutine。
使用范例:
fun main() = runBlocking {
launch {
println("launch Start")
delay(500)
println("launch End")
}
println("Done")
}
我们发现,这段程序码会先执行最外层的 println("Done")
,接着才是 launch()
里面的函式。
这是因为当使用 launch()
时,会建立一个新的 Coroutine,成为 runBlocking()
的子 Coroutine。如果在使用 launch()
时,没有带入 Coroutine context ,那麽预设会使用的调度器为 Dispatchers.Default ,也就是背景运算的 coroutine。所以就会出现 println("Done")
先执行的情况。
launch()
改为 coroutineScope()
会发生什麽事呢?fun main() = runBlocking {
coroutineScope {
println("launch Start")
delay(500)
println("launch End")
}
println("Done")
}
→ 结果则是按照原本的顺序来执行。因为利用 coroutineScope()
并不会建立新的 coroutine 而是继承外层的 coroutine context,也就是说,在 coroutineScope()
里面与外面的 println("Done")
其实都是在同一个 coroutine,所以会按照其顺序来执行。
在 launch()
里有三行程序,除了 delay(500)
是 suspend 函式以外,其他两行程序都是一般的程序码,所以我们可以将 launch()
内部的程序码重构,把这里面的程序码抽取出去。
suspend fun launchFun() {
println("launch Start")
delay(500)
println("launch End")
}
因为这三行程序码有包含一行 suspend 函式 - delay()
,所以这个函式必须要加上 suspend
来修饰。
suspend 函式只能在 CoroutineScope 或是被另一个 suspend 函式调用。
launch()
的回传值是 Job
, Job
是代表一个可被取消的任务,我们可以呼叫 cancel()
取消该 coroutine 的执行。
如下:
fun main() = runBlocking {
val job = launch {
repeat(100_000) { index ->
println("launch Start $index")
delay(500)
println("launch End $index")
}
}
delay(1100)
job.cancel()
println("Done")
}
→ 在上面的范例中,我们在 launch
中执行了一段会执行十万次的任务,这个任务首先先印出 launch Start $index
,接着暂停该 Coroutine 500 毫秒,在 500 毫秒暂停时间结束之後,就会列印出 launch End $index
。
→ 我们把 launch 的回传值储存在 job
变数上,在外层的 coroutine 执行 1100 毫秒之後,就呼叫 job.cancel()
把十万次的任务停止。
在这个范例中,Coroutine 的执行区块可以分成两块,红色区块我称为 「Coroutine1」,粉紫色区块我称为「Coroutine2」。我们可以看一下它执行的时间轴。
Coroutine1 因为调用 delay(1100)
,所以 Coroutine1 暂停 1100 毫秒,在暂停之後调用了 Coroutine2 的 cancel()
,所以 Coroutine2 的任务被取消,当 Coroutine2 的任务被取消之後, Coroutine1 就能继续被执行。
在前面的范例中,我们在 runBlocking
中建立一个 launch
,在 launch
所产生的 coroutine 就是在 runBlocking
里面的子 coroutine,所以当我们执行时,预设是会先执行外层的 coroutine,接着才是内层的 coroutine 。
所以下面的范例会先执行 println("Done")
,接着才会执行 launch
内部的任务。
fun main() = runBlocking {
launch {
println("launch Start")
delay(500)
println("launch End")
}
println("Done")
}
假如我们希望能够先完成 launch
里面的任务,完成之後我们才接续执行下面的任务,我们可以使用 job.join()
。
fun main() = runBlocking {
val job = launch {
println("launch Start")
delay(500)
println("launch End")
}
job.join()
println("Done")
}
在 Kotlin 的 Coroutine 中,提供了 joinAll()
来同时针对多个 Job 来呼叫其 join()
。
fun main() = runBlocking {
val job = launch {
println("launch Start")
delay(500)
println("launch End")
}
joinAll(job)
println("Done")
}
其实 joinAll()
只是呼叫带入 Job 的 join()
。
public suspend fun joinAll(vararg jobs: Job): Unit = jobs.forEach { it.join() }
如同 launch()
, async()
也是用来建立 Coroutine 的,只不过与 launch()
不同的是, async()
是用来处理有回传值的非同步任务,而且它回传的是 Deferred
而不是 Job
。
fun main() = runBlocking{
val deferred = async {
println("async start")
delay(500)
println("async end")
50
}
val deferredValue = deferred.await()
println("done $deferredValue")
}
我们将 async
回传的值存到变数 deferred
里面,这时候 async
里面的任务还没有开始执行,等到呼叫 await()
之後,就会执行 async
,并且在这边等待 async
完成,所以这边回传的值就会是 async
区块中的回传值,如上方范例的 50。
在 Kotlin 中,lambda 的最後一行就是它的回传值,我们可以省略
return
,不过如果要使用return
也可以。
return@async 50
await()
的使用类似 join()
。前面有提到, join()
是会把该 Coroutine 任务完成之後,才会继续往下走, await()
的用法也是一样,当呼叫 await()
的时候,该 Coroutine 就会开始执行,直到结束或是发生例外。与 join()
不同的是, await()
是会回传在 async
区块的回传值,如上方的 50。
值得注意的是,我们在 async
并没有宣告回传的资料型别, Kotlin 会自动做型别推断。当然我们也可以自行加上型别。如下:
async<Int> {
println("async start")
delay(500)
println("async end")
50
}
不过 IDE 会提示你把它移除,因为 Kotlin 会自动型别推断。
await()
?async()
与 launch()
一样,都是立刻被排程来执行,如果没有使用 await()
,在执行这段程序时,也会执行 async()
。
await()
拿掉:fun main() = runBlocking{
async {
println("async start")
delay(500)
println("async end")
return@async 50
}
println("done")
}
→ 执行顺序就会跟使用 launch
一样。
前面我们看了两个 coroutine builder : launch()
、 async()
,我们知道当程序执行到这边的时候,就会将这两个 builder 所建造出来的 coroutine 排进执行的行程中。所以它们预设是立刻就被呼叫的。
有的,我们只需要在使用 launch()
、 async()
时带入 CoroutineStart.LAZY
即可。
如下:
fun main() = runBlocking {
launch(start = CoroutineStart.LAZY) {
println("launch Start")
delay(500)
println("launch End")
}
println("Done")
}
→ 加上 CoroutineStart.LAZY 之後, launch()
里面的任务就不会立刻执行了。不过,如果没有启动 launch()
那麽程序就会在这边一直等它执行。
job.start()
来主动启动 Coroutine 的执行。fun main() = runBlocking {
val job = launch(start = CoroutineStart.LAZY) {
println("launch Start")
delay(500)
println("launch End")
}
job.start()
println("Done")
}
→ 这边我们也可以使用 job.join()
来启动。
与 launch()
相同,我们也可以替 async()
加上 CoroutineStart.LAZY 来让 Coroutine 延後启动。
async()
可以使用 await()
、 start()
或是 join()
来启动。
其中, await()
是有包含回传值得,其他两个没有。
函式有分有回传值的以及没有回传值的, 当然 suspend 函式也有,Coroutine 提供了两种 Coroutine Builder 来处理这两种不同的 suspend 函式,没有回传值的对应的是 launch()
Builder,而有回传值的对应的是 async()
。虽然这两个 Coroutine builder 回传的值不一样, launch()
回传的是 Job
,而 async()
回传的是 Deferred
。但是其实这两种回传值都本质上都是一样的,都是一个可以取消的背景任务。
Job
与 Deferred
共同的函式有 cancel()
、 start()
、 join()
。
其中 cancel()
用来取消 coroutine, start()
用来启动 coroutine,而 join()
则是会让 coroutine 的任务完成之後,才把後面的工作加入。
因为 Deferred
是包含回传值的,所以我们可以使用 await()
来取得 coroutine scope 的回传值。
最後最後, launch()
以及 async()
都是在执行後立刻会被排进执行的顺序。如果想要延後才执行,就要在使用这两个函式的时候带入 CoroutineStart.LAZY。
Kotlin Taiwan User Group
Kotlin 读书会
<<: Day 10 : Docker基本操作 Volume篇
Hashicorp Nomad: resources 在Kubernets的 Quality of ...
v-for 基於数组、物件透过迭代、遍历对前端进行渲染。 item in items items: ...
人的科技文明发展始终来自於人性 在数位的新时代浪潮席卷之下,世界各国不论是个人的发展,还是组织企业团...
今天提到的是 side effect,就像之前提到的, side effect 通常出现在以下几种情...
我们知道写程序有个阶段就是一个输入、运算处理、输出 网页是由HTML、CSS、Javascript三...