Para conservar los cálculos correctos de Swift cuando fallan tareas hermanas, captura el error de cada cálculo dentro de su tarea hija y devuelve un valor que represente el resultado. Este ejemplo de línea de comandos para Mac recopila todos los resultados, conserva el orden de entrada y mantiene disponibles los valores correctos.
La tarea es un pequeño cálculo por lotes sintético: elevar al cuadrado una lista de enteros no negativos e indicar qué entradas se procesaron correctamente. Las entradas negativas y el desbordamiento de enteros producen fallos individuales. Todos los datos de cálculo permanecen en memoria; el programa imprime los resultados en Terminal.
¿Por qué un error puede interrumpir la recopilación de resultados?
Una tarea hija que lanza un error no cancela de inmediato a sus tareas hermanas. El límite decisivo es el cuerpo del grupo de tareas: cuando un error escapa de ese cuerpo, Swift cancela las tareas hijas restantes. Apple documenta esta distinción en su referencia de withThrowingTaskGroup(of:returning:isolation:body:).
Por lo tanto, un bucle de iteración que lanza errores y tiene un bloque catch externo puede dejar de recopilar resultados al detectar el primer error. Es posible que ya se hayan almacenado valores anteriores, pero el bucle no se reanuda después de ese bloque catch. Las tareas hijas restantes reciben la cancelación si el error escapa del cuerpo del grupo.
Para cálculos independientes, define explícitamente la política de fallos. El ejemplo siguiente usa un grupo de tareas que no lanza errores, cuyos hijos devuelven un resultado correcto, un fallo o una cancelación. El cálculo en sí sigue lanzando errores; su envoltorio convierte ese error en un valor antes de que llegue al grupo.
Esto se ajusta al límite que documenta Swift: los hijos de un grupo de tareas que no lanza errores no pueden propagar errores lanzados, así que deben gestionarlos internamente o representar su resultado como un valor devuelto. Consulta la documentación de TaskGroup de Apple.
Define qué debe devolver cada entrada
El contrato del lote es un resultado por cada posición de entrada. Un cálculo correcto devuelve el cuadrado exacto de un entero. Un cálculo rechazado devuelve una descripción del fallo. Un cálculo omitido porque se observó una cancelación devuelve un resultado de cancelación.
| Situación | Resultado | Comportamiento del lote |
|---|---|---|
Entrada no negativa cuyo cuadrado cabe en Int64 |
Correcto, con el cuadrado | Conservar el valor |
| Entrada negativa | Fallo: negativeInput |
Seguir recopilando los demás resultados |
El cuadrado supera el rango de Int64 |
Fallo: overflow |
Seguir recopilando los demás resultados |
| Se observa una cancelación antes del cálculo | Cancelado | Conservar los resultados completados y omitir el trabajo pendiente |
Rechazar las entradas negativas es una regla ilustrativa de la aplicación; matemáticamente, los números negativos se pueden elevar al cuadrado. Mantener esa regla separada del desbordamiento permite que el ejemplo muestre dos condiciones de fallo distintas.
Para multiplicar, usa multipliedReportingOverflow(by:) y comprueba su indicador de desbordamiento. Apple especifica que el valor parcial devuelto es el producto completo solo cuando ese indicador es falso; de lo contrario, está truncado. El ejemplo descarta ese valor truncado y lanza un error. Consulta Int64.multipliedReportingOverflow(by:).
Cada resultado también incluye el índice de su entrada. Swift no garantiza el orden de finalización de las tareas hijas, así que añadir solo los valores completados no conserva el orden original. La guía del lenguaje describe este comportamiento de planificación en Concurrency.
Ejecuta el ejemplo completo en tu Mac
Usa un Mac con Swift 6 o una cadena de herramientas posterior. En Terminal, ejecuta swift --version para comprobar el compilador instalado, tal como recomienda la guía de introducción de Swift.org. Si Swift no está disponible, sigue el enlace de instalación de esa guía antes de continuar.
Guarda el siguiente programa completo como Calculations.swift en una carpeta de trabajo. Importa Foundation para elegir un límite inicial de concurrencia a partir del número de procesadores activos.
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)")
}
}
Desde esa carpeta, compila y ejecuta el programa:
swiftc -swift-version 6 -parse-as-library Calculations.swift -o Calculations
./Calculations
La opción -parse-as-library permite que este único archivo fuente use su punto de entrada explícito @main. El ejecutable realiza los cálculos sin leer ni escribir datos de la aplicación. Swift no estaba disponible en el entorno de investigación, así que se revisó este programa, pero no se compiló ni ejecutó allí.
Para las entradas proporcionadas sin cancelación, el resultado esperado es:
[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]
El orden impreso se obtiene ordenando por índice de entrada después de la recopilación. El orden de ejecución puede variar sin cambiar esta presentación. La matriz de valores correctos se construye después, por lo que el informe completo de resultados sigue disponible para su inspección.
¿Cómo conserva el recopilador todos los resultados correctos?
La tarea principal es propietaria de la matriz de resultados. Cada tarea hija captura un índice y una entrada inmutable, calcula su resultado y devuelve una fila. Las tareas hijas nunca añaden elementos directamente a la matriz de la tarea principal. Esto sigue el enfoque de la guía del lenguaje, que acumula los resultados de las tareas hijas dentro del cuerpo del grupo de tareas. Consulta Concurrency.
Al principio, el recopilador envía una cantidad limitada de tareas hijas. Cada fila completada libera una posición para otra entrada. Por tanto, un cálculo fallido hace avanzar el lote igual que uno correcto: ambos devuelven una fila ordinaria y la recopilación continúa.
activeProcessorCount de Foundation informa del número de núcleos de procesamiento activos disponibles en el equipo. El ejemplo limita ese número a cuatro como decisión ilustrativa de planificación. Ese límite no es un valor óptimo medido ni garantiza que se ejecuten cuatro cálculos simultáneamente. Consulta la documentación de activeProcessorCount de Apple.
El límite de concurrencia restringe la cantidad de tareas hijas pendientes, mientras que la matriz de resultados sigue creciendo con el número de entradas. Esto es adecuado para un lote pequeño en un equipo de escritorio. Para estos cuadrados tan pequeños, el ejemplo muestra cómo gestionar los resultados; no afirma tener un rendimiento superior al de un bucle secuencial.
La comprobación de entradas vacías devuelve una matriz vacía. Un límite de concurrencia igual a cero o negativo se ajusta a uno. Los valores de entrada duplicados se mantienen por separado porque su identidad depende de la posición, así que dos entradas iguales siguen produciendo dos filas.
¿Qué ocurre cuando se cancela la tarea principal?
La cancelación de Swift es cooperativa. Al cancelar una tarea principal, la cancelación se propaga a sus tareas hijas estructuradas, pero cada hija debe observar esa señal y responder. La documentación de TaskGroup de Apple explica tanto la propagación como el requisito de gestionar la cancelación de forma cooperativa.
Aquí, evaluate llama a Task.checkCancellation() antes de calcular. Apple documenta que este método lanza CancellationError cuando detecta una cancelación; el envoltorio lo captura por separado y devuelve una fila de cancelación. Consulta checkCancellation().
Cuando el recopilador observa una cancelación, marca como canceladas las entradas que aún no se han enviado. Las tareas hijas que ya se enviaron siguen procesándose hasta terminar. Sus resultados correctos permanecen en el informe, y las tareas hijas que observan la cancelación devuelven filas de cancelación.
La cancelación puede llegar después de que una tarea hija haga su comprobación. Esa tarea aún podría completarse correctamente, algo que este contrato acepta. Si el cálculo se convierte en un bucle largo, añade comprobaciones de cancelación en intervalos adecuados dentro del cálculo. El grupo sigue esperando a que terminen todas las tareas hijas enviadas antes de devolver el resultado, tal como se documenta para TaskGroup.
¿Cuándo conviene usar un grupo de tareas que lanza errores?
Si las tareas hijas existentes ya lanzan errores y quieres conservar esa interfaz, un grupo de tareas que lanza errores también permite recopilar resultados individuales. Llama repetidamente a nextResult() y evalúa cada resultado correcto o fallo devuelto hasta que el resultado opcional esté vacío. A diferencia de next(), este método devuelve un valor Result en lugar de lanzar al llamador el error de la tarea hija. Apple documenta su comportamiento respecto al orden de finalización en nextResult().
Para ese diseño, incluye la identidad de la entrada en los valores correctos y en cualquier error que necesite atribución. Un error aritmético sin más datos no identifica cuál de varias entradas lo produjo. Elige también una política de cancelación explícita, en lugar de tratar la cancelación como un fallo de cálculo ordinario.
Usa el enfoque que devuelve resultados cuando el requisito principal es contar con un informe completo por entrada. Usa un grupo que lanza errores cuando el comportamiento deseado del lote es propagar un error fuera del grupo, o cuando resulta más conveniente consumir operaciones existentes que lanzan errores mediante nextResult().
Para esta tarea de cálculo local, el contrato esencial es sencillo: conservar cada valor correcto, registrar cada entrada fallida y seguir recopilando resultados. Llevar el índice de entrada en cada resultado hace que el informe final sea útil, incluso cuando los cálculos terminan en un orden impredecible.
Fuentes
- withThrowingTaskGroup(of:returning:isolation:body:): documentación para desarrolladores de Apple.
- TaskGroup: documentación para desarrolladores de Apple.
- Concurrency: The Swift Programming Language, proyecto Swift.
- multipliedReportingOverflow(by:): documentación para desarrolladores de Apple.
- Getting Started: Swift.org.
- activeProcessorCount: documentación para desarrolladores de Apple.
- checkCancellation(): documentación para desarrolladores de Apple.
- nextResult(): documentación para desarrolladores de Apple.