DEV Community

GoyesDev
GoyesDev

Posted on

[SC] Limitar el número de tareas en ejecución dentro de un TaskGroup

¿Por qué no es suficiente que el thread pool cooperativo de Swift limite los hilos por sí solo?

Si bien es cierto que el pool de hilos cooperativo de Swift maneja los hijos, cada vez que se invoca addTask sobre TaskGroup, se programa inmediatamente un nuevo sub-Task. En el caso de que se creen muchas peticiones simultáneas (e.g. descargar 1000 imágenes), igual se tienen que separar recursos tanto en el dispositivo móvil como quizás en un servidor remoto (en el caso de que se haya hecho una consulta web).

¿En qué situaciones reales es necesario limitar el número de tareas concurrentes?

  • Al procesar archivos grandes en paralelo, se tienen que almacenar todos los datos en RAM, lo que puede disparar picos en la memoria.
  • Al hacer muchas peticiones simultáneas a un servidor, este puede estrangular al cliente (ralentizando respuestas, rechazando algunas peticiones o bloqueando temporalmente).
  • Hay recursos como bases de datos o manejadores de archivos que tienen límites naturales, que excederlos, provoca fallos.

¿Cómo funciona el patrón de "ventana deslizante" (sliding window)?

A continuación, pongo el algoritmo de ejecución concurrente con ventana deslizante:

let photoURLs: [URL] = getPhotos()
// ⚠️ se tiene que usar TaskGroup y no DiscardingTaskGroup para poder tener acceso a .next() y así poder controlar el paralelismo
let images: [UIImage] = await withTaskGroup(of: UIImage.self) { group in
  // ⚠️ pending de tipo Array<URL>.SubSequence para poder hacer popFirst()
  var pending = photoURLs[...]
  let maxConcurrency = 3
  var results: [UIImage] = []

  // ⚠️ ciclo for inicia el primer lote
  for _ in 0..<maxConcurrency {
    guard let url = pending.popFirst() else { break }
    group.addTask { await downloadPhoto(url: url) }
  }

  // ⚠️ Se suspende hasta que una de las tareas en curso termina
  while let image = await group.next() {
    // ⚠️ Se almacena el resultado
    results.append(image)
    // ⚠️ Si aún hay URLs disponibles, entonces se dispara una nueva tarea
    if let url = pending.popFirst() {
      group.addTask { await downloadPhoto(url: url) }
    }
  }

  // ⚠️ Al final se retornan los resultados aunque no se garantiza que el orden final sea igual al orden inicial de las tareas. En caso de que eso sea necesario, se pueden indexar los resultados para ordenarlos al final
  return results
}
Enter fullscreen mode Exit fullscreen mode

¿Qué hace group.next() y por qué es clave para el throttling?

group.next() permite esperar hasta que una tarea despachada acabe para poder empezar la siguiente. Se necesita para poder limitar el paralelismo.

¿Qué rol cumple ArraySlice y popFirst() en el patrón?

var pending = photoURLs[...] convierte el arreglo original en un ArraySlice.

Un ArraySlice es una "vista" del arreglo. No copia los datos, sino que simplemente apunta a una porción del arreglo original. Esto lo hace eficiente en memoria.

Por otro lado, popFirst() hace dos cosas a la vez:

  1. Retorna el primer elemento del slice.
  2. Lo elimina de la colección.

pending trabaja como una cola de trabajo pendiente. Si popFirst() retorna nil es porque no hay más trabajo por encolar.

¿Cómo se recolectan los resultados manteniendo la concurrencia limitada?

Se espera a que termine una tarea del grupo con group.next(). El valor retornado se guarda en un arreglo de resultado. Si hay un valor más en la cola de trabajo pendiente, entonces se inicia.

Cuando ya no haya más valores en la cola de trabajo pendiente, y ya no haya más tareas para esperar en el grupo, entonces el bucle while se termina y se retorna el arreglo de resultados.


Recite

  • ¿Puedes explicar con tus propias palabras el flujo del patrón sliding window paso a paso?
  • ¿Qué diferencia hay entre el loop for inicial y el loop while en el código?
  • ¿Qué ocurre con las tareas restantes si una tarea en withThrowingTaskGroup lanza un error?

Review

  • ¿Por qué withDiscardingTaskGroup no es compatible con este patrón de throttling?
  • ¿Cómo garantizarías el orden original de los resultados si fuera necesario?
  • ¿Cuál es la solución recomendada cuando necesitas throttling sin valores de retorno?

Bibliografía

Top comments (0)