Para manter os cálculos bem-sucedidos do Swift quando tarefas irmãs falharem, capture o erro de cada cálculo dentro da tarefa filha e retorne um valor que represente o resultado. Este exemplo de linha de comando para Mac coleta todos os resultados, preserva a ordem das entradas e mantém os valores bem-sucedidos disponíveis.

A tarefa é um pequeno cálculo em lote sintético: elevar ao quadrado uma lista de números inteiros não negativos e informar quais entradas foram processadas com sucesso. Entradas negativas e estouro de inteiro geram falhas individuais. Todos os dados dos cálculos ficam na memória; o programa imprime os resultados no Terminal.

Por que um erro pode interromper a coleta de resultados?

Uma tarefa filha que lança um erro não cancela imediatamente as tarefas irmãs. O limite relevante é o corpo do grupo de tarefas: quando um erro escapa desse corpo, o Swift cancela as tarefas filhas restantes. A Apple documenta essa distinção na referência de withThrowingTaskGroup(of:returning:isolation:body:).

Consequentemente, um loop de iteração que lança erros e tem um tratamento externo pode parar de coletar resultados no primeiro erro observado. Valores anteriores talvez já tenham sido armazenados, mas o loop não continua depois desse tratamento. As tarefas filhas restantes recebem um sinal de cancelamento se o erro escapar do corpo do grupo.

Para cálculos independentes, deixe explícita a política de falhas. O exemplo abaixo usa um grupo de tarefas que não lança erros, cujas tarefas filhas retornam sucesso, falha ou cancelamento. O cálculo em si ainda lança erros; seu invólucro converte esse erro em um valor antes que ele chegue ao grupo.

Isso se enquadra no limite documentado pelo Swift: tarefas filhas de um grupo que não lança erros não podem propagar erros lançados, então precisam tratá-los internamente ou representar o resultado como um valor de retorno. Consulte a documentação de TaskGroup da Apple.

Defina o que cada entrada deve retornar

O contrato do lote prevê um resultado por posição de entrada. Um cálculo bem-sucedido retorna o quadrado exato do inteiro. Um cálculo rejeitado retorna uma descrição da falha. Um cálculo ignorado porque foi observado um cancelamento retorna um resultado de cancelamento.

Situação Resultado Comportamento do lote
Entrada não negativa cujo quadrado cabe em Int64 Sucesso contendo o quadrado Manter o valor
Entrada negativa Falha: negativeInput Continuar coletando os outros resultados
O quadrado excede o intervalo de Int64 Falha: overflow Continuar coletando os outros resultados
Cancelamento observado antes do cálculo Cancelado Manter os resultados concluídos e ignorar o trabalho pendente

Rejeitar entradas negativas é uma regra de aplicação ilustrativa; matematicamente, números negativos podem ser elevados ao quadrado. Manter essa regra separada do estouro faz com que o exemplo demonstre duas condições de falha distintas.

Para a multiplicação, use multipliedReportingOverflow(by:) e verifique seu indicador de estouro. A Apple especifica que o valor parcial retornado é o produto completo somente quando esse indicador é falso; caso contrário, ele é truncado. O exemplo descarta esse valor truncado e lança um erro. Consulte Int64.multipliedReportingOverflow(by:).

Cada resultado também contém o índice da entrada. O Swift não garante a ordem de conclusão das tarefas filhas, então apenas adicionar os valores concluídos a uma lista não preserva a ordem original. O guia da linguagem descreve esse comportamento de agendamento em Concorrência.

Execute o exemplo completo no Mac

Use um Mac com a cadeia de ferramentas Swift 6 ou posterior. No Terminal, execute swift --version para verificar o compilador instalado, conforme recomendado pelo guia de introdução do Swift.org. Se o Swift não estiver disponível, siga o link de instalação nesse guia antes de continuar.

Salve o programa completo a seguir como Calculations.swift em uma pasta de trabalho. Ele importa Foundation para escolher um limite inicial de concorrência com base na quantidade de processadores ativos.

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)")
    }
}

Nessa pasta, compile e execute o programa:

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

A opção -parse-as-library permite que esse único arquivo de código-fonte use seu ponto de entrada explícito @main. O executável realiza os cálculos sem ler nem gravar dados da aplicação. O Swift não estava disponível no ambiente de pesquisa, então o programa foi inspecionado, mas não foi compilado nem executado ali.

Para as entradas fornecidas sem cancelamento, a saída esperada é:

[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]

A ordem impressa vem da ordenação por índice de entrada após a coleta. A ordem de execução pode variar sem alterar essa apresentação. A lista de valores bem-sucedidos é criada em seguida, deixando o relatório completo de resultados disponível para inspeção.

Como o coletor mantém todos os resultados bem-sucedidos?

A tarefa pai é proprietária do vetor de resultados. Cada tarefa filha captura um índice e uma entrada imutável, calcula seu resultado e retorna uma linha. As tarefas filhas nunca adicionam elementos diretamente ao vetor da tarefa pai. Isso segue a abordagem do guia da linguagem, que acumula os resultados das tarefas filhas dentro do corpo do grupo de tarefas. Consulte Concorrência.

Inicialmente, o coletor envia um número limitado de tarefas filhas. Cada linha concluída libera uma posição para outra entrada. Portanto, um cálculo que falha faz o lote avançar da mesma forma que um cálculo bem-sucedido: ambos retornam uma linha comum, e a coleta continua.

activeProcessorCount do Foundation informa o número de núcleos de processamento ativos disponíveis no computador. O exemplo limita esse número a quatro como uma escolha ilustrativa de agendamento. Esse limite não é um ótimo medido nem uma garantia de que quatro cálculos serão executados simultaneamente. Consulte a documentação de activeProcessorCount da Apple.

O limite de concorrência restringe o número de tarefas filhas pendentes, enquanto o vetor de resultados continua crescendo conforme aumenta o número de entradas. Isso é adequado para um pequeno lote em um computador desktop. Para esses quadrados tão simples, o exemplo demonstra o tratamento de resultados; ele não afirma ter melhor desempenho do que um loop sequencial.

A verificação de entrada vazia retorna um vetor vazio. Um limite de concorrência igual a zero ou negativo é ajustado para um. Entradas duplicadas continuam separadas porque sua identidade vem da posição; assim, duas entradas iguais ainda produzem duas linhas.

O que acontece quando a tarefa pai é cancelada?

O cancelamento no Swift é cooperativo. Cancelar uma tarefa pai propaga o cancelamento às tarefas filhas estruturadas, mas uma tarefa filha precisa observar esse sinal e responder. A documentação de TaskGroup da Apple explica tanto a propagação quanto a necessidade de tratamento cooperativo.

Aqui, evaluate chama Task.checkCancellation() antes de calcular. A Apple documenta que esse método lança CancellationError quando o cancelamento é observado; o invólucro o captura separadamente e retorna uma linha de cancelamento. Consulte checkCancellation().

Quando o coletor observa o cancelamento, marca como canceladas as entradas que ainda não foram enviadas. As tarefas filhas já enviadas continuam sendo aguardadas até a conclusão. Seus resultados bem-sucedidos permanecem no relatório, e as tarefas filhas que observarem o cancelamento retornam linhas de cancelamento.

O cancelamento pode chegar depois da verificação de uma tarefa filha. Essa tarefa ainda pode ser concluída com sucesso, o que este contrato aceita. Se o cálculo passar a conter um loop longo, adicione verificações de cancelamento em intervalos úteis dentro dele. Conforme documentado para TaskGroup, o grupo ainda espera todas as tarefas filhas enviadas terminarem antes de retornar.

Quando usar um grupo de tarefas que lança erros?

Se as tarefas filhas existentes já lançam erros e você quer preservar essa interface, um grupo de tarefas que lança erros também permite coletar resultados individuais. Chame nextResult() repetidamente e use um switch para cada sucesso ou falha retornado, até que o resultado opcional seja vazio. Diferentemente de next(), esse método retorna um valor Result em vez de lançar o erro da tarefa filha para quem o chamou. A Apple documenta o comportamento de ordem de conclusão em nextResult().

Nesse modelo, inclua a identidade da entrada nos valores bem-sucedidos e em quaisquer erros que precisem ser atribuídos. Um erro aritmético sem mais informações não identifica qual de várias entradas o produziu. Escolha também uma política explícita de cancelamento, em vez de tratar o cancelamento como uma falha comum de cálculo.

Use a abordagem que retorna resultados quando o principal requisito é um relatório completo por entrada. Use um grupo que lança erros quando propagar um erro para fora do grupo é o comportamento desejado para o lote, ou quando for mais conveniente consumir operações existentes que lançam erros por meio de nextResult().

Para esta tarefa de cálculo local, o contrato essencial é simples: manter cada valor bem-sucedido, registrar cada entrada com falha e continuar coletando. Levar o índice da entrada em todos os resultados torna o relatório final útil mesmo quando os cálculos terminam em uma ordem imprevisível.

Fontes