DEV Community

GoyesDev
GoyesDev

Posted on

Sincronizando tareas con DispatchGroup

Framework: Dispatch


Un DispatchGroup en GCD sirve para sincronizar varias tareas concurrentes y ejecutar algo cuando todas hayan terminado.

En el siguiente ejemplo se usa notify(queue:work:) para sincronizar las tareas despachadas con queue.async. Observar que debo marcar explícitamente el ingreso al grupo antes de llamar queue.async con group.enter(). Además, una vez que haya terminado el trabajo asíncrono, debo llamar group.leave() dentro de queue.async.

import Foundation
let group = DispatchGroup()
let queue = DispatchQueue.global(qos: .userInitiated)
for i in 1...3 {
  // Inicio de la tarea
  group.enter()
  queue.async {
    print("Iniciando tarea \(i)")
    // Simulando trabajo
    Thread.sleep(forTimeInterval: Double(i))
    print("Terminando tarea \(i)")
    // Terminando la tarea
    group.leave()
  }
}
// Notificar cuando todas las tareas terminan
group.notify(queue: .main) {
  print("Todas las tareasterminaron")
}
Enter fullscreen mode Exit fullscreen mode

Marcar inicio y fin del bloque forma implícita

También se puede manejar el enter/leave de forma implícita con async(group:execute:), que implica que hay un enter() al principio del bloque del código y un leave() al final del bloque. Tener en cuenta que este llamado no sirve cuando se usan closures para reportar el fin de una tarea asíncrona. Por ejemplo:

import Foundation
let group = DispatchGroup()
let queue = DispatchQueue.global(qos: .userInitiated)
for i in 1...3 {
  queue.async(group: group) {
    print("Iniciando tarea \(i)")
    // Simulando trabajo
    Thread.sleep(forTimeInterval: Double(i))
    print("Terminando tarea \(i)")
  }
}
// Notificar cuando todas las tareas terminan
group.notify(queue: .main) {
    print("Todas las tareasterminaron")
}
Enter fullscreen mode Exit fullscreen mode

Sincronización bloqueante

Aparte de notify, también puedo usar wait() que bloquea el hilo actual.

// Notificar cuando todas las tareas terminan sin bloquear
group.notify(queue: .main) {
    print("Todas las tareasterminaron")
}
// Bloquea hasta que todas las tareas terminan
group.wait()
print("Se imprime después de sincronizar")
Enter fullscreen mode Exit fullscreen mode

Balance estricto enter/leave

Cada group.enter() debe tener su correspondiente group.leave(). Si se llama enter() más veces que leave(), el grupo nunca podrá sincronizarse (se queda colgado). Por otro lado, si llamo más leave() que enter(), la app puede estallarse con un EXC_BAD_INSTRUCTION.

Enter antes de lanzar la tarea

El llamado al enter() debe ocurrir ANTES de iniciar la tarea asíncrona, no dentro.

La siguiente implementación está correcta:

group.enter()
queue.async {
 // ...
 group.leave()
}
Enter fullscreen mode Exit fullscreen mode

Cubrir todos los caminos de ejecución

Si la tarea puede terminar por varias causas (éxito, error, cancelación, etc), hay que asegurarse de llamar leave() en todos los casos.

group.enter()
URLSession.shared.dataTask(with: url) { data, response, error in
    // garantizar leave al salir, pase lo que pase
    defer { group.leave() }
    if let error = error {
        print("Error:", error)
        return
    }
    print("Descargado:", data?.count ?? 0)
}.resume()
Enter fullscreen mode Exit fullscreen mode

Aquí defer en Swift es un patrón muy útil para no olvidar el leave().

Cola de sincronización de tareas

El parámetro queue en el group.notify(queue:) se refiere a qué cola va a ejecutar el bloque de código de sincronización cuando todas las tareas del grupo terminen.

group.notify(queue: .main) {
    print("Todas listas en MAIN thread: \(Thread.current)")
    // Aquí puedo actualizar UI
}
group.notify(queue: .global(qos: .userInitiated)) {
    print("Todas listas en cola GLOBAL: \(Thread.current)")
    // Aquí puedo seguir con trabajo en background
}
Enter fullscreen mode Exit fullscreen mode

Si quiero actualizar la interfaz gráfica, el notify debe ir en .main. Si quiero seguir procesando datos, puedo usar otra cola global concurrente.

Top comments (0)