範圍化取消與傳播(scoped cancellation and propagation)
你派三名跑者去三家店問價,打算用最先回報的那個。只要一人回來,另外兩趟差事就沒意義了——你會想把他們叫回來,而不是任他們繼續跑、白費力氣。範圍化取消正是為此而設的機制:一種對一個任務、以及巢狀在它之下的所有任務發出訊號、要它們提早停下並乾淨收尾的辦法。傳播則是讓訊號往下傳到後代、又把結果往上傳的那一塊。
現代非同步裡的取消幾乎總是協作式的,而非強制的。你不會在指令執行到一半時粗暴地殺掉一個任務——那會洩漏鎖和寫到一半的狀態。取而代之,取消請求設下一個旗標(一個取消權杖,或在 Rust 裡就只是丟棄那個 future)。任務在它的下一個讓出點察覺:每個 await 同時也檢查「我被取消了嗎?」,若是就停下、跑它的清理(關檔、釋放鎖、解構式/Drop),然後收尾。「範圍化」意味著訊號順著結構走:取消一個任務範圍,會遞迴地取消其內生出的每個子任務,於是你用一個請求就取消整棵子樹。傳播雙向流動:取消請求往下流到後代;而某個任務的失敗或逾時可以往上流到它的範圍,範圍再去取消手足——這就是「先到者贏、取消其餘」的模式。
這之所以重要,是因為沒有它,並行程式會洩漏失控的工作:逾時觸發了,緩慢的操作卻還在空轉;一個失敗的請求拋下它做到一半的幫手。範圍化取消把任務的生命期綁到一個期限、一個父層、或一場「誰先完成」的競賽上,並保證輸家會被收掉。兩個誠實的要點:因為它是協作式的,一個永遠到不了讓出點的任務(緊密的 CPU 迴圈、或阻塞程式碼)無法被取消——它得抵達某個 await 才能察覺。而取消必須正確跑完清理,否則你只是拿一個洩漏的任務換來一個洩漏的檔案描述符或一把被握住的鎖;取消安全性是真實的設計課題,並非免費。
帶逾時的競賽(Rust/Tokio):tokio::select! { r = work() => r, _ = sleep(timeout) => give_up() } // 當計時器贏了,work() 的 future 被丟棄——取消會在下一個 await 跑它的清理。
丟棄輸的那個 future 會協作式地取消它;它在下一個讓出點停住並乾淨收尾。
協作式取消無法打斷一個從不讓出的任務——阻塞呼叫或 CPU 迴圈會無視訊號。而被取消的任務仍須跑完它的清理;草率的取消會洩漏鎖和檔案描述符。「取消安全」是你要設計出來的性質,不是預設。