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
- withThrowingTaskGroup(of:returning:isolation:body:) — Documentação para desenvolvedores da Apple.
- TaskGroup — Documentação para desenvolvedores da Apple.
- Concorrência — A linguagem de programação Swift, projeto Swift.
- multipliedReportingOverflow(by:) — Documentação para desenvolvedores da Apple.
- Introdução — Swift.org.
- activeProcessorCount — Documentação para desenvolvedores da Apple.
- checkCancellation() — Documentação para desenvolvedores da Apple.
- nextResult() — Documentação para desenvolvedores da Apple.