學了 launch 與 async 之後,我們已經能在一個 Coroutine 範圍裡建立多個子任務。下一個自然的問題是:這些子任務跟外層 Coroutine 的關係是什麼?什麼時候算結束?發生錯誤時又該由誰負責?這就是結構化併發(Structured Concurrency)要回答的事情。
從一個熟悉的例子開始#
回顧最早寫過的那段 Coroutine:
suspend fun showContents() = coroutineScope {
launch {
val token = login(userName, password)
val content = fetch(token)
withContext(Dispatchers.Main) {
showContent(content)
}
}
}launch 區塊內呼叫的 suspend 函式 login() 與 fetch(),會繼承 launch 的 Coroutine context,使用同一個調度器(Dispatcher)執行。範例沒有指定,所以預設會落在 Dispatchers.Default,也就是背景執行緒。
結構化併發的核心觀念#
Kotlin Coroutine 採用結構化的併發模型。在這個模型下,Coroutine 之間會形成父子關係(Parent-Child Relationship):
- 最外層的是父 Coroutine。
- 在父 Coroutine 中再建立的就是子 Coroutine。
- 父 Coroutine 必須等所有子 Coroutine 完成之後,自己才會結束。
把這個規則套到上面的例子,整段 showContents() 的執行時間,會從 login() 一路涵蓋到 showContent() 結束,外層不會「先跑完」就放著子 Coroutine 不管。
為什麼這樣設計#
Roman Elizarov(Kotlin Coroutines team lead)在介紹結構化併發時強調:併發任務應該像函式呼叫一樣有清楚的進入點與離開點。這帶來幾個好處:
- 生命週期可預測:父 Coroutine 結束時,所有子 Coroutine 一定也結束了,不會有任務「漏在外面」繼續執行。
- 錯誤可追蹤:子 Coroutine 拋出的例外,會沿著父子關係往上傳播,讓錯誤有明確的處理位置。
- 取消有層次:取消父 Coroutine 等於同時取消所有子 Coroutine,不需要逐一管理。
結構化併發要解的是一個老問題:背景任務「啟動了卻找不到了」。在沒有結構化的設計裡,你一旦把工作丟到 thread pool,就很難確定它什麼時候結束、誰來收尾。Coroutine 把任務的層級關係納入語言模型,讓這件事變得明確。
父子關係下的「結束」#
在結構化併發中,結束的定義不是「外層程式跑到最後一行」,而是「外層加上所有後代 Coroutine 都做完」。配合前面提到的:
- 預設情況下,子 Coroutine 會繼承父 Coroutine 的 context。
- 父 Coroutine 結束前會等待所有子 Coroutine。
- 父 Coroutine 被取消(Cancellation)時,所有子 Coroutine 也會跟著被取消。
這幾條規則是後續討論 Job、SupervisorJob、例外處理的基礎。下一章開始我們就會看到,當其中一個子任務出錯,這套父子關係怎麼運作。
小結#
- 結構化併發強調 Coroutine 之間有清楚的父子關係,父 Coroutine 會等所有子 Coroutine 結束才結束。
- 子 Coroutine 預設繼承父 Coroutine 的 Coroutine context(包含調度器)。
- 這個設計讓生命週期、錯誤傳播、取消都有層次可循,避免背景任務遺漏在外。
原文出處#
- 原書/iThome:https://ithelp.ithome.com.tw/articles/10263540