join() 的功能是:讓目前的 Coroutine 暫停,直到指定的 Job 完成才繼續往下走。當有多個 Job 要一起等的時候,可以改用 joinAll()

join 的基本用法#

fun main() = runBlocking {
    val job = launch {
        delay(100)
        println("inner")
    }
    job.join()
    println("outer")
}

如果沒有 job.join(),外層會比 launch 內的程式先執行;加上 job.join() 後,外層必須等子 Coroutine 結束才會印出 outer

joinAll:一次等多個 Job#

joinAll() 等價於對每個 Job 各自呼叫一次 join()

suspend fun joinAll(vararg jobs: Job)
suspend fun Collection<Job>.joinAll()

它提供兩種多載:可變參數版本,以及對 Collection<Job> 的擴充函式。

fun main() = runBlocking {
    val jobs = mutableListOf<Job>()
    repeat(10) {
        jobs.add(launch {
            delay(100)
            println("inner: $it: ${Thread.currentThread().name}")
        })
    }
    jobs.joinAll()
    println("outer: ${Thread.currentThread().name}")
}

外層會等到全部子 Coroutine 都結束之後才印出 outer

其中一個 Job 被取消#

如果其中某些 Job 被取消,joinAll() 會等所有 Job「結束」(不論完成或取消)後才回到外層。下例用 withTimeout 限制每個子 Coroutine 的執行時間:

fun main() = runBlocking {
    val jobs = mutableListOf<Job>()
    repeat(10) {
        jobs.add(launch {
            withTimeout(500) {
                delay(100L * it)
                println("inner: $it: ${Thread.currentThread().name}")
            }
        })
    }
    jobs.joinAll()
    println("outer: ${Thread.currentThread().name}")
}

由於 delay 時間是 100L * it,從 it = 6 起會超過 500 毫秒、被 withTimeout 取消,因此只會看到 it = 05 的輸出。

其中一個 Job 拋例外#

當 launch 中拋出例外時,可以用 CoroutineExceptionHandler 接住未處理的例外:

class Day14 {
    private val scope = CoroutineScope(Job() + CoroutineExceptionHandler { _, e -> println(e) })
    suspend fun tryException() {
        val jobs = mutableListOf<Job>()
        repeat(10) {
            jobs.add(scope.launch {
                delay(100L)
                yield()
                println("inner: $it: ${Thread.currentThread().name}")
                if (it == 2) {
                    throw RuntimeException("Incorrect")
                }
            })
        }
        jobs.joinAll()
        println("outer: ${Thread.currentThread().name}")
    }
}

it == 2 的子 Coroutine 拋出 RuntimeException,由 CoroutineExceptionHandler 印出,其他子 Coroutine 仍會跑完,最後外層才印出 outer

awaitAll:對應 async 的版本#

針對多個 Deferred,可以使用 awaitAll() 一次取得所有結果。它一樣提供可變參數與 Collection 兩種多載,回傳一個 List<T>

fun main() = runBlocking {
    val deferred = mutableListOf(
        async {
            delay(100)
            println("inner: async1")
            1
        },
        async {
            delay(150)
            println("inner: async2")
            2
        }
    )
    val result = deferred.awaitAll().sum()
    println("outer: $result")
}

要把多個 async 的結果合併,就看自己想怎麼處理;上例使用 sum() 把整數結果加總,輸出 outer: 3

awaitAll 的例外處理方式與單一 await() 類似,使用 try-catch 包覆即可。

cancelAndJoin:cancel 後再 join#

實務上經常一起呼叫 cancel()join()

job.cancel()
job.join()

Kotlin Coroutine 提供了 cancelAndJoin() 把這兩步合併:

public suspend fun Job.cancelAndJoin() {
    cancel()
    return join()
}

把上例改寫:

fun main() = runBlocking {
    val job = launch {
        println("inside: ${Thread.currentThread().name}")
        withContext(Dispatchers.Default) {
            delay(200)
            println("Default: ${Thread.currentThread().name}")
        }
    }
    delay(100)
    job.cancelAndJoin()
    println("outer: ${Thread.currentThread().name}")
}

語意一樣,但程式更精簡。

原文出處#

  • 原書/iThome:https://ithelp.ithome.com.tw/articles/10267485