結構化並行與任務範圍(structured concurrency and the task scope)
想想函式呼叫本來就如何運作:若 main() 呼叫 helper(),helper() 必須先完成,main() 才能越過那一行繼續。控制流有乾淨的巢狀——子任務在父任務內部完成。現在留意,單純的「go 生一個背景任務」破壞了這點:你射出並行工作,函式就回返,留下一個無人負責、四處亂跑的孤兒任務。結構化並行為並行任務恢復了這種巢狀:每個任務都在一個範圍內被生出,而該範圍在其內部生出的所有任務都完成之前不會結束。
其機制是一個任務範圍——也叫保育室(Trio 的用語)或任務群組。你開啟一個範圍,在其中生出一個或多個並行任務,而在範圍的結束邊界,程式碼會等它們全部完成才繼續往下。因為範圍是一個語彙區塊,並行任務的生命期受它約束:沒有任務能活得比建立它的區塊久。三個性質自動隨之而來。任務不會洩漏——它們在範圍結尾被會合。錯誤會傳播——若某個子任務失敗,範圍可以取消它的手足、把錯誤重新拋給父層,而不是讓失敗消失在一個脫離的任務裡。取消是有範圍的——取消父層會遞迴地取消其下生出的一切。
為何這很重要:無結構的生成,給並行帶來的問題,正如 goto 給控制流帶來的問題——任務從某處開始、卻不在任何特定處結束,錯誤被默默吞掉,資源被無人追蹤的任務握著。結構化並行把並行的生命期化為程式碼形狀裡看得見的東西,就像大括號讓呼叫的生命期看得見一樣。它是 Trio 的保育室、Kotlin 的協程範圍、Java 的 StructuredTaskScope、以及 Swift 任務群組背後的組織思想。誠實的提醒:它刻意換掉一些彈性——一個真正「射後不理」、本就該活得比建立者久的背景任務,並不適合這個模型,你必須刻意走出它(或傳入一個壽命更長的範圍)。
Trio 風格的保育室:async with open_nursery() as n: n.start_soon(fetch_a); n.start_soon(fetch_b); # 在兩者都完成之前這個 with 區塊不會結束;若其一拋出錯誤,另一個會被取消、錯誤在此處傳播。
範圍在其邊界會合所有子任務;沒有任務逃出區塊,而一個子任務的失敗能取消它的手足。
結構化並行不會讓任務照順序跑,也不免除對共享資料同步的需要——範圍內的並行任務照樣會競爭。它管的是它們的生命期、錯誤傳播與取消,而非它們對共享記憶體的存取。