SC #11: Task Groups
Source: Dev.to
TaskGroup
Un TaskGroup contiene subtareas creadas dinámicamente, que pueden ejecutarse de forma serial o concurrente. El grupo solo se considera terminado cuando todas las subtareas hayan finalizado.
withTaskGroup(of:returning:isolation:body:) permite crear el contexto para agregar subtareas usando addTask(priority:operation:).
Ejemplo básico: descarga de imágenes
func downloadPhoto(url: URL) async -> UIImage {
// …
}
await withTaskGroup(of: UIImage.self) { taskGroup in
let photoURLs = await listPhotoURLs(inGallery: "Vacaciones")
for photoURL in photoURLs {
taskGroup.addTask {
await downloadPhoto(url: photoURL)
}
}
}
El TaskGroup actúa como un forEach que ejecuta cada subtarea de forma concurrente, almacenando los resultados.
Recopilando resultados
Como colección
Se puede definir el tipo de retorno como una colección (por ejemplo, [UIImage].self). Después de iniciar todas las subtareas, se usa la AsyncSequence del grupo para esperar los resultados y construir la colección resultante.
let images: [UIImage] = await withTaskGroup(
of: UIImage.self,
returning: [UIImage].self
) { taskGroup in
let photoURLs = await listPhotoURLs(inGallery: "Vacaciones")
for photoURL in photoURLs {
taskGroup.addTask {
await downloadPhoto(url: photoURL)
}
}
var images = [UIImage]()
for await result in taskGroup {
images.append(result)
}
return images
}
Usando reduce
Como TaskGroup conforma AsyncSequence, también se puede emplear el operador reduce:
await withTaskGroup(of: UIImage.self, returning: [UIImage].self) { taskGroup in
let photoURLs = try! await listPhotoURLs(inGallery: "Vacaciones")
for photoURL in photoURLs {
taskGroup.addTask {
try! await downloadPhoto(url: photoURL)
}
}
return await taskGroup.reduce(into: [UIImage]()) { partialResult, image in
partialResult.append(image)
}
}
ThrowingTaskGroup
Cuando alguna subtarea puede arrojar un error, se utiliza withThrowingTaskGroup(of:returning:isolation:body:).
try await withThrowingTaskGroup(of: UIImage.self, returning: [UIImage].self) { taskGroup in
let photoURLs = try await listPhotoURLs(inGallery: "Vacaciones")
for photoURL in photoURLs {
taskGroup.addTask {
try await downloadPhoto(url: photoURL)
}
}
return try await taskGroup.reduce(into: [UIImage]()) { partialResult, image in
partialResult.append(image)
}
}
Comportamiento ante errores
- Un
ThrowingTaskGroupno falla automáticamente cuando una subtarea lanza una excepción; la subtarea se marca como fallida, pero el grupo continúa ejecutándose. - Para propagar el error y cancelar las tareas restantes, es necesario desempaquetar explícitamente cada subtarea con
group.next().
try await withThrowingTaskGroup(of: Void.self) { group in
group.addTask { throw SomeError() }
// El grupo no falla aquí
}
try await withThrowingTaskGroup(of: Void.self) { group in
group.addTask { throw SomeError() }
try await group.next() // Propaga el error y cancela tareas en curso
}
next()
group.next() entrega los resultados de las subtareas una a una, en orden aleatorio, y lanza el error correspondiente si la subtarea falla. Esto permite manejar cada error de forma individual.
while let result = try await taskGroup.next() {
print("Got a new result:", result)
}
Cancelación de tareas
- Se puede cancelar todo el conjunto de tareas con
group.cancelAll(). - Si se agrega una tarea a un grupo ya cancelado mediante
addTask(priority:operation:), la tarea se cancela inmediatamente después de ser creada. Para evitar que la tarea siquiera comience, useaddTaskUnlessCancelled(priority:operation:).
for photoURL in photoURLs {
let didAddTask = taskGroup.addTaskUnlessCancelled {
try await downloadPhoto(url: photoURL)
}
print("Added task: \(didAddTask)")
}