若要在同級任務失敗時保留成功的 Swift 計算結果,請在子任務內捕捉每次計算的錯誤,並回傳一個結果值。這個 Mac 命令列範例會收集所有結果、保留輸入順序,並讓成功的值維持可用。

這項任務是小型的合成批次計算:將一串非負整數平方,並回報哪些輸入成功。負數輸入和整數溢位會各自造成失敗。所有計算資料都留在記憶體中;程式會在終端機列印結果。

為什麼一個錯誤會中斷結果收集?

拋出錯誤的子任務不會立即取消同級任務。關鍵界線在任務群組的主體:當錯誤逸出該主體時,Swift 會取消其餘子任務。Apple 在 withThrowingTaskGroup(of:returning:isolation:body:) 的參考文件中說明了這項區別。

因此,若在迴圈中拋出錯誤,並由外層的 catch 捕捉,收集作業可能會在首次觀察到錯誤時停止。先前的值可能已經儲存,但迴圈不會在該 catch 之後恢復執行。如果錯誤逸出群組主體,其餘子任務就會收到取消訊號。

對於彼此獨立的計算,請明確訂定失敗處理方式。下方範例使用不會拋出錯誤的任務群組,讓子任務回傳成功、失敗或取消狀態。計算本身仍會拋出錯誤;其包裝函式會在錯誤抵達群組前,將它轉換成一個值。

這符合 Swift 文件所述的界線:不會拋出錯誤的任務群組中的子任務無法傳播拋出的錯誤,因此必須在內部處理,或將結果表示為回傳值。請參閱 Apple 的 TaskGroup 文件。

定義每個輸入必須回傳的結果

批次契約規定每個輸入位置都對應一個結果。成功的計算會回傳精確的整數平方。遭拒的計算會回傳失敗說明。若因觀察到取消訊號而略過計算,則會回傳取消結果。

狀況 結果 批次行為
平方值可容納於 Int64 的非負輸入 包含平方值的成功結果 保留該值
負數輸入 失敗:negativeInput 繼續收集其他結果
平方值超出 Int64 範圍 失敗:overflow 繼續收集其他結果
計算前觀察到取消訊號 已取消 保留已完成的結果並略過待處理工作

拒絕負數輸入是為了示範應用程式規則;負數在數學上也可以平方。將這項規則與溢位分開,可以讓範例展示兩種不同的失敗狀況。

進行乘法時,請使用 multipliedReportingOverflow(by:) 並檢查其溢位旗標。Apple 指出,只有在旗標為 false 時,回傳的部分值才是完整乘積;否則該值會遭截斷。此範例會丟棄遭截斷的值並拋出錯誤。請參閱 Int64.multipliedReportingOverflow(by:)。

每個結果也都會附帶其輸入索引。Swift 不保證子任務的完成順序,因此只附加已完成的值無法保留原始順序。語言指南在並行處理一節說明了此排程行為。

在 Mac 上執行完整範例

請使用配備 Swift 6 或更新版工具鏈的 Mac。在終端機執行 swift --version,依照 Swift.org 的入門指南建議,檢查已安裝的編譯器。如果無法使用 Swift,請先依照該指南中的安裝連結完成安裝,再繼續操作。

將下列完整程式儲存為工作資料夾中的 Calculations.swift。此程式會匯入 Foundation,以根據目前啟用的處理器數量選擇初始並行上限。

import Foundation

enum CalculationError: Error {
    case negativeInput
    case overflow
}

enum Outcome: Sendable {
    case success(Int64)
    case failure(String)
    case cancelled
}

struct Row: Sendable {
    let index: Int
    let outcome: Outcome
}

func square(_ input: Int64) throws -> Int64 {
    guard input >= 0 else {
        throw CalculationError.negativeInput
    }
    let product = input.multipliedReportingOverflow(by: input)
    guard !product.overflow else {
        throw CalculationError.overflow
    }
    return product.partialValue
}

func evaluate(index: Int, input: Int64) -> Row {
    do {
        try Task.checkCancellation()
        let value = try square(input)
        return Row(index: index, outcome: .success(value))
    } catch is CancellationError {
        return Row(index: index, outcome: .cancelled)
    } catch {
        return Row(
            index: index,
            outcome: .failure(String(describing: error))
        )
    }
}

func collect(
    _ inputs: [Int64],
    maxConcurrent: Int
) async -> [Row] {
    guard !inputs.isEmpty else { return [] }
    let width = min(inputs.count, max(1, maxConcurrent))

    return await withTaskGroup(of: Row.self) { group in
        var rows: [Row] = []
        rows.reserveCapacity(inputs.count)

        for index in 0..<width {
            let input = inputs[index]
            group.addTask {
                evaluate(index: index, input: input)
            }
        }
        var nextIndex = width

        while let row = await group.next() {
            rows.append(row)

            if Task.isCancelled {
                while nextIndex < inputs.count {
                    rows.append(Row(
                        index: nextIndex,
                        outcome: .cancelled
                    ))
                    nextIndex += 1
                }
            } else if nextIndex < inputs.count {
                let index = nextIndex
                let input = inputs[index]
                nextIndex += 1
                group.addTask {
                    evaluate(index: index, input: input)
                }
            }
        }

        return rows.sorted { $0.index < $1.index }
    }
}

@main
struct CalculationDemo {
    static func main() async {
        let inputs: [Int64] = [3, -2, 4, Int64.max, 0]
        let width = min(
            4,
            max(1, ProcessInfo.processInfo.activeProcessorCount)
        )
        let rows = await collect(inputs, maxConcurrent: width)
        var successes: [Int64] = []

        for row in rows {
            let input = inputs[row.index]
            switch row.outcome {
            case .success(let value):
                successes.append(value)
                print("[\(row.index)] \(input): success \(value)")
            case .failure(let message):
                print("[\(row.index)] \(input): failure \(message)")
            case .cancelled:
                print("[\(row.index)] \(input): cancelled")
            }
        }
        print("Successful values: \(successes)")
    }
}

在該資料夾中編譯並執行程式:

swiftc -swift-version 6 -parse-as-library Calculations.swift -o Calculations
./Calculations

-parse-as-library 選項可讓這個單一原始檔使用明確的 @main 進入點。此可執行檔會進行計算,不會讀取或寫入應用程式資料。研究環境中無法使用 Swift,因此此程式僅經過檢視,並未在該環境中編譯或執行。

使用提供的輸入且不進行取消時,預期輸出如下:

[0] 3: success 9
[1] -2: failure negativeInput
[2] 4: success 16
[3] 9223372036854775807: failure overflow
[4] 0: success 0
Successful values: [9, 16, 0]

收集後依輸入索引排序,即可得到上述列印順序。執行順序可能有所不同,但不會影響呈現結果。成功值陣列會在之後建立,因此完整結果報告仍可供檢視。

收集器如何保留每個成功結果?

父任務負責持有結果陣列。每個子任務會擷取一個索引和一個不可變輸入,計算其結果,並回傳一筆資料列。子任務不會直接附加至父任務的陣列。這遵循語言指南所述,在任務群組主體內累積子任務結果的做法。請參閱並行處理。

收集器一開始只會提交有限數量的子任務。每完成一筆資料列,就空出一個位置供另一個輸入使用。因此,計算失敗時,批次仍會以與成功相同的方式推進:兩者都會回傳一般資料列,收集作業會繼續進行。

Foundation 的 activeProcessorCount 會回報電腦上可用的作用中處理核心數量。此範例將該數量限制為四,作為示範性的排程選擇。這項上限並非經測量得出的最佳值,也不保證會同時執行四項計算。請參閱 Apple 的 activeProcessorCount 文件。

並行上限會限制尚未完成的子任務數量,但結果陣列仍會隨輸入數量增加。這適用於小型桌面批次。由於這些平方計算非常簡單,此範例用來展示結果處理方式;它並未宣稱效能優於循序迴圈。

空輸入的防護條件會回傳空陣列。並行上限若為零或負數,則會調整為一。重複的輸入值仍會分開處理,因為識別依據是位置,所以兩個相同輸入仍會產生兩筆資料列。

父任務取消時會發生什麼事?

Swift 的取消機制採用合作式設計。取消父任務會將取消訊號傳播給其結構化子任務,但子任務必須觀察到訊號並作出回應。Apple 的 TaskGroup 文件說明了訊號傳播,以及合作式處理的要求。

此處,evaluate 會在計算前呼叫 Task.checkCancellation()。Apple 文件指出,觀察到取消時,此方法會拋出 CancellationError;包裝函式會分開捕捉此錯誤,並回傳一筆已取消的資料列。請參閱 checkCancellation()。

收集器一旦觀察到取消,就會將尚未提交的輸入標記為已取消。已提交的子任務仍會被全部收集完成。其成功結果會保留在報告中,而觀察到取消訊號的子任務則會回傳已取消的資料列。

取消訊號可能在子任務完成檢查後才送達。該子任務仍可能成功完成,而此契約接受這種情況。如果計算會變成長時間迴圈,請在計算過程中適當的間隔加入取消檢查。如 TaskGroup 文件所述,群組仍會等待所有已提交的子任務完成後才回傳。

什麼時候應該改用會拋出錯誤的任務群組?

若現有子任務已會拋出錯誤,而且你想保留該介面,也可以使用會拋出錯誤的任務群組來收集個別結果。反覆呼叫 nextResult(),並對每個回傳的成功或失敗結果執行 switch,直到選擇性結果為空。與 next() 不同,此方法會回傳 Result 值,而不是將子任務的錯誤拋給呼叫端。Apple 在 nextResult() 中說明了其依完成順序處理的行為。

採用這種設計時,請在成功值和需要追蹤來源的錯誤中加入輸入識別資訊。單純的算術錯誤無法指出多個輸入中的哪一個造成錯誤。也請明確選擇取消處理政策,不要將取消視為一般計算失敗。

若主要需求是取得每個輸入的完整報告,請使用回傳結果值的方式。若希望將錯誤從群組中往外傳播,或透過 nextResult() 使用現有的拋出錯誤操作較方便,則請使用會拋出錯誤的群組。

對於這項本機計算任務,核心契約很簡單:保留每個成功值、記錄每個失敗輸入,並持續收集。讓每個結果都帶有輸入索引,即使計算完成順序難以預測,最終報告仍然實用。

來源