DEV Community

GoyesDev
GoyesDev

Posted on

[GCD] DQ Concurrente: ejecución asíncrona

Esta es la combinación que produce paralelismo real en GCD: una cola concurrente despachando trabajo con async. El llamador no espera (por ser async) y las tareas pueden correr al mismo tiempo en hilos distintos (por ser la cola concurrente).

Consideremos el siguiente fragmento de código, donde se despachan tres tareas con async sobre una cola concurrente:

let concurrentQueue = DispatchQueue(label: "dev.goyes.concurrente", attributes: .concurrent)

for i in 1...3 {
  concurrentQueue.async {
    print("Tarea \(i) empieza, hilo: \(Thread.current)")
  }
}
print("Las tres tareas ya se encolaron")
Enter fullscreen mode Exit fullscreen mode

A diferencia de la cola serial con async (donde el orden entre "Tarea 1", "Tarea 2" y "Tarea 3" sí estaba garantizado), aquí no hay ninguna garantía de orden entre esas tres líneas, ni entre ellas y "Las tres tareas ya se encolaron". El FIFO de la cola garantiza el orden en que GCD entrega las tres tareas para ejecución, pero una vez entregadas, cada una corre en un hilo independiente cuyo arranque real lo decide el scheduler del sistema operativo, no la cola.

Reentrada: sigue siendo segura

Igual que en la cola serial, llamar async desde un closure que ya está corriendo en la misma cola concurrente no produce deadlock. async nunca bloquea, así que solo agrega trabajo a la cola y continúa.

Consideremos el siguiente fragmento de código, donde se llama async de forma anidada sobre la misma cola concurrente:

let concurrentQueue = DispatchQueue(label: "dev.goyes.concurrente", attributes: .concurrent)

concurrentQueue.async {
  print("Tarea externa")
  concurrentQueue.async { // Seguro, no hay deadlock
    print("Tarea interna")
  }
}
Enter fullscreen mode Exit fullscreen mode

A diferencia de lo que podría parecer, "Tarea externa" sí está garantizado que se imprime completa antes de que "Tarea interna" pueda empezar, y eso no depende de que la cola sea serial o concurrente. La razón es orden de programa: print("Tarea externa") y la llamada anidada a async son dos líneas secuenciales dentro del mismo closure, ejecutadas una después de la otra en el mismo hilo. La llamada anidada ni siquiera somete "Tarea interna" a la cola hasta que print("Tarea externa") ya retornó, así que no hay ningún punto en el que ambas estén compitiendo por ejecutarse. La cola concurrente sí introduce una carrera real cuando varias tareas se someten de forma independiente, sin que una espere a que la otra termine de ejecutar su cuerpo completo: es el caso del ejemplo anterior con el for y las tres tareas async.

El riesgo: nada serializa el acceso a estado compartido

Combinar async con una cola concurrente, sobre estado mutable compartido, produce una carrera de datos si no hay ningún otro mecanismo de sincronización de por medio.

Consideremos el siguiente fragmento de código, donde una clase UnsafeCounter incrementa un contador compartido desde una cola concurrente:

final class UnsafeCounter {
  private let queue = DispatchQueue(label: "dev.goyes.counter", attributes: .concurrent)
  private var value = 0

  func increment() {
    queue.async {
      self.value += 1 // ⚠️ Lectura + suma + escritura no es una operación atómica
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Cada llamado a increment() retorna de inmediato y encola una escritura. Si varios hilos del pool concurrente ejecutan self.value += 1 al mismo tiempo, pueden leer el mismo valor antes de que el otro termine de escribir, y una de las dos sumas se pierde. Llamar increment() 100 veces desde distintos hilos no garantiza que value termine en 100.

Esta es la situación exacta que DispatchBarrier y DispatchSemaphore existen para resolver, y que se cubre en los siguientes artículos.

Lo que viene

Falta ver DispatchWorkItem, una forma de empaquetar el trabajo que se despacha en un objeto en lugar de un closure suelto. Es útil cuando se necesita cancelar ese trabajo o esperar a que termine desde otro punto del código. Después de eso, DispatchGroup, DispatchBarrier y DispatchSemaphore cubren cómo coordinar y proteger trabajo concurrente como el de este artículo.


Bibliografía

Top comments (0)