在前一篇文章我們知道 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 讀書會
有興趣的讀者歡迎參考:https://coroutine.kotlin.tips/
天瓏書局