iT邦幫忙

2021 iThome 鐵人賽

DAY 7
1
Software Development

Coroutine 停看聽系列 第 7

Day7:CoroutineScope:launch() 以及 async()

在前一篇文章我們知道 suspend 函式必須要在 Coroutine scope 裏面才能執行,本篇文章我們來了解一下兩個 Coroutine Builder : launch()async()

launch()

如果一個 Coroutine 的回傳值沒有回傳值,也就是回傳 Unit 時,就可以使用 launch() 來建立一個 Coroutine。

使用範例:

fun main() = runBlocking {
    launch {
        println("launch Start")
        delay(500)
        println("launch End")
    }
    println("Done")
}

launch1

我們發現,這段程式碼會先執行最外層的 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")
}

Coroutine Scope

→ 結果則是按照原本的順序來執行。因為利用 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 函式調用。

Job

cancel()

launch() 的回傳值是 JobJob 是代表一個可被取消的任務,我們可以呼叫 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() 把十萬次的任務停止。

那麼,會怎麼執行呢?

launch job

launch job

在這個範例中,Coroutine 的執行區塊可以分成兩塊,紅色區塊我稱為 「Coroutine1」,粉紫色區塊我稱為「Coroutine2」。我們可以看一下它執行的時間軸。

launch job timeline

Coroutine1 因為調用 delay(1100) ,所以 Coroutine1 暫停 1100 毫秒,在暫停之後調用了 Coroutine2 的 cancel() ,所以 Coroutine2 的任務被取消,當 Coroutine2 的任務被取消之後, Coroutine1 就能繼續被執行。

join()

在前面的範例中,我們在 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")
}

launch join

joinAll()

在 Kotlin 的 Coroutine 中,提供了 joinAll() 來同時針對多個 Job 來呼叫其 join()

fun main() = runBlocking {
    val job = launch {
        println("launch Start")
        delay(500)
        println("launch End")
    }
    joinAll(job)
    println("Done")
}

joinAll()

其實 joinAll() 只是呼叫帶入 Job 的 join()

public suspend fun joinAll(vararg jobs: Job): Unit = jobs.forEach { it.join() }

async()

await()

如同 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 await

我們將 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")
}

async without await

→ 執行順序就會跟使用 launch 一樣。


延時任務

前面我們看了兩個 coroutine builder : launch()async() ,我們知道當程式執行到這邊的時候,就會將這兩個 builder 所建造出來的 coroutine 排進執行的行程中。所以它們預設是立刻就被呼叫的。

那麼有沒有一種方法可以讓我們延後才執行呢?

有的,我們只需要在使用 launch()async() 時帶入 CoroutineStart.LAZY 即可。

如下:

launch() 的延時執行

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.start

→ 這邊我們也可以使用 job.join() 來啟動。

async() 的延時任務

launch() 相同,我們也可以替 async() 加上 CoroutineStart.LAZY 來讓 Coroutine 延後啟動。

async() 可以使用 await()start() 或是 join() 來啟動。

其中, await() 是有包含回傳值得,其他兩個沒有。

小結

函式有分有回傳值的以及沒有回傳值的, 當然 suspend 函式也有,Coroutine 提供了兩種 Coroutine Builder 來處理這兩種不同的 suspend 函式,沒有回傳值的對應的是 launch() Builder,而有回傳值的對應的是 async() 。雖然這兩個 Coroutine builder 回傳的值不一樣, launch() 回傳的是 Job ,而 async() 回傳的是 Deferred 。但是其實這兩種回傳值都本質上都是一樣的,都是一個可以取消的背景任務。

JobDeferred 共同的函式有 cancel()start()join()

其中 cancel() 用來取消 coroutine, start() 用來啟動 coroutine,而 join() 則是會讓 coroutine 的任務完成之後,才把後面的工作加入。

因為 Deferred 是包含回傳值的,所以我們可以使用 await() 來取得 coroutine scope 的回傳值。

最後最後, launch() 以及 async() 都是在執行後立刻會被排進執行的順序。如果想要延後才執行,就要在使用這兩個函式的時候帶入 CoroutineStart.LAZY。

心智圖

CoroutineScope mind map

特別感謝

Kotlin Taiwan User Group
Kotlin 讀書會


上一篇
Day6:三大要素
下一篇
Day8:結構化併發 (Structured Concurrency)
系列文
Coroutine 停看聽30

尚未有邦友留言

立即登入留言