Swift 6.2 的 Approachable Concurrency 如何遷移並行程式碼?
1. 題目場景與限制
你維護一個圖片編輯 App。快取由 UI 存取,網路下載、解碼和縮圖產生耗時較長。舊程式依賴隱式執行緒切換,部分 API 是 nonisolated async,測試只覆蓋最後圖片是否正確。現在要採用 Swift 6.2,並保持捲動順暢、結果可重現,同時逐步開啟 Swift 6 的資料競爭檢查。
請先說明會記錄哪些基線:主執行緒影格時間、解碼吞吐量、並行工作數、快取命中率、取消延遲,以及遷移前後的編譯器診斷數量。沒有基線,很難區分效能回歸與真正修正。
2. 先解釋 Swift 6.2 的變化
Swift 6.2 提供可選的 -default-isolation MainActor,讓目標中的程式預設隔離到主 actor;也引入 @concurrent,明確標記需要平行執行的函式。啟用 NonisolatedNonsendingByDefault 後,nonisolated async 預設沿用呼叫者的 actor,不會自動跳到全域並行執行器;需要保留平行語意時再使用 @concurrent。
這些選項減少註解雜訊,但不會讓所有工作自動平行。預設隔離描述誰能安全存取狀態,@concurrent 才表達「這裡允許離開目前 actor 並行執行」。
3. 設計隔離邊界
把可變快取和 UI 狀態放在 @MainActor 型別中,把純值轉換和不可變輸入輸出的解碼函式設計為 Sendable。下載層只回傳值型別,避免把帶有隱含執行緒親和性的物件跨 actor 傳遞。每個 API 都寫清楚三件事:呼叫者 actor、受保護的狀態、是否允許平行。
例如快取可以維持主 actor 保護:
@MainActor
final class ImageCache {
private var values: [URL: Image] = [:]
func value(for url: URL) -> Image? { values[url] }
func insert(_ image: Image, for url: URL) { values[url] = image }
}不要用 MainActor.run 到處「包住」舊程式來消除警告;遷移指南把它定位為過渡工具,長期應在 API 型別與簽名上表達隔離要求。
4. 何時使用 @concurrent
只有當工作可以安全平行執行、輸入輸出符合傳送要求,且基準證明平行有收益時,才為函式加上 @concurrent。圖片解碼通常符合這個條件:它不直接讀寫主 actor 快取,回傳新建立的值。
@MainActor
struct ImagePipeline {
static func image(from url: URL) async throws -> Image {
let cached = cache.value(for: url)
if let cached { return cached }
let image = try await decode(url: url)
cache.insert(image, for: url)
return image
}
@concurrent
private static func decode(url: URL) async throws -> Image {
let (data, _) = try await URLSession.shared.data(from: url)
return try ImageDecoder.decode(data)
}
}面試時要指出:@concurrent 不是「更快」開關。過度使用會增加排程、記憶體和同步成本;若解碼庫本身串行或圖片很小,吞吐量可能下降。
5. 處理呼叫者上下文與 Sendable
啟用呼叫者上下文語意後,一個 async 函式可能繼續在呼叫者 actor 上執行。它適合存取呼叫者隔離的狀態,但長時間 CPU 工作會阻塞該 actor,因此應拆出 @concurrent 的純計算階段。跨 actor 的參數和結果必須是 Sendable,或由 actor/鎖等同步邊界保護。
不要透過 @unchecked Sendable 靜音所有診斷。只有在能證明內部不變量、同步原語和生命週期後,才把它作為窄範圍的最後手段,並配套並行壓力測試。
6. 分階段遷移與工具鏈
先在單一模組啟用即將到來的特性和嚴格診斷,利用遷移工具產生警告與 fix-it;再穩定公共 API 的隔離和 Sendable 約束,最後切換 Swift 6 語言模式。每個階段都保留可回滾的提交,並按模組歸檔編譯器診斷。
建議順序是:值型別與協定約束、底層服務、快取 actor、UI 呼叫層。這樣錯誤會在邊界處暴露,避免一次改動數百個 await 後無法定位。舊客戶端需要相容時,可先維護 Swift 5 模式介面,再逐步提高實作模組的檢查級別。
7. 如何驗證沒有競爭或意外串行化
功能測試之外,加入並行壓力:同時請求同一 URL、隨機取消、重複寫入和跨 actor 讀取;用 Thread Sanitizer 與嚴格並行診斷捕捉競態。效能測試分別覆蓋冷快取、熱快取、不同圖片尺寸和不同並行上限,比較影格時間、吞吐量、峰值記憶體與取消延遲。
檢查工作上下文和非同步呼叫堆疊,確認解碼階段確實平行、快取讀寫仍在主 actor。若開啟 @concurrent 後影格時間沒有改善,優先檢查解碼器是否阻塞、工作是否被單一鎖串行化,以及是否產生過多短工作。
8. 評分要點與追問
必須說清
- 區分預設隔離、呼叫者上下文和明確平行;不要把
async等同於背景執行緒。 - 以
Sendable、actor 邊界和可取消性證明安全,而不是靠@unchecked Sendable消除錯誤。 - 給出分階段遷移、壓力測試和效能基線,能解釋回歸訊號。
常見追問
- 如果解碼庫不是執行緒安全,你會放在 actor、串行 executor 還是程序外服務?為什麼?
- 如何限制同一 URL 的重複下載,同時不讓整個快取串行?
- 遷移期間 Swift 5 與 Swift 6 模組如何共用 API,哪些約束必須先發布?
評分參考
優秀答案能把隔離建模、遷移工具和執行時證據連成閉環:先定義狀態與執行上下文,再用最小的 @concurrent 提升平行度,最後用編譯器、Sanitizer 和基準資料驗證結論。