Un TaskGroup contiene subtareas creadas dinámicamente, que pueden ejecutarse de forma serial o concurrente. Un TaskGroup solo se considera terminado cuando todas las subtareas hayan terminado.
withTaskGroup(of:returning:isolation:body:) permite crear el contexto para agregar las subtareas usando addTask(priority:operation:).
Considerar el siguiente ejemplo donde se descargan varias imágenes de una galería:
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 funciona como una especie de forEach que ejecuta cada subtarea de forma concurrente, almacenando los resultados.
Retornando una colección
Se puede definir el tipo de retorno como una colección (e.g. [UIImage].self). Luego de iniciar todas las subtareas, se usa async sequence para esperar los resultados y encadenarlos en 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
}
Como TaskGroup conforma AsyncSequence entonces también se puede usar 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, name in
partialResult.append(name)
}
}
Manejando errores del TaskGroup
En caso de que alguna subtarea pueda arrojar un error, se puede crear un ThrowingTaskGroup con 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, name in
partialResult.append(name)
}
}
Manejo de errores de una subtarea dentro de un ThrowingTaskGroup
Cuando una subtarea arroja un error, el ThrowingTaskGroup NO FALLA. Simplemente la subtarea se marca como fallida, pero eso no necesariamente implica que el grupo de tareas falle.
El siguiente ThrowingTaskGroup NO FALLA:
try await withThrowingTaskGroup(of: Void.self) { group in
group.addTask { throw SomeError() }
}
Para detectar un fallo es necesario desempaquetar explícitamente cada tarea con group.next(), lo que provoca que se vuelva a arrojar el error y que se cancele cualquier tarea en curso.
try await withThrowingTaskGroup(of: Void.self) { group in
group.addTask { throw SomeError() }
try await group.next()
}
next()
group.next() recibe los errores de las subtareas individuales, permitiendo darles el manejo adecuado.
next() entrega un resultado a la vez, en orden aleatorio; y arroja un error si la subtarea falla.
while let result = try await taskGroup.next() {
print("Got a new result:", result)
}
Mutación externa
No se debe cambiar un TaskGroup desde fuera del Task donde se creó.
Cancelar un TaskGroup
Se puede cancelar un conjunto de tareas usando group.cancelAll() sobre el TaskGroup.
Si se agrega una tarea a un grupo cancelado con addTask(priority:operation:), esta se cancela inmediatamente después de ser creada. No obstante, solo detendrá su trabajo si verifica correctamente la cancelación (con checkCancellation() o isCancelled). Se debe usar addTaskUnlessCancelled(priority:operation:) para evitar que la tarea empiece si el grupo ya está cancelado.
for photoURL in photoURLs {
let didAddTask = taskGroup.addTaskUnlessCancelled {
try await downloadPhoto(url: photoURL)
}
print("Added task: \(didAddTask)")
}
Top comments (0)