DEV Community

GoyesDev
GoyesDev

Posted on

SC #11: Task Groups

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) }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
  }
}
Enter fullscreen mode Exit fullscreen mode

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() }
}
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)")
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)