DEV Community

GoyesDev
GoyesDev

Posted on

[GCD] DQ Serial: ejecución síncrona y sincronización de hilos

Despachar un closure con sync bloquea el hilo que hace el llamado hasta que ese closure termina de ejecutarse. El control no regresa al código que llamó a sync hasta que el trabajo encolado se completó.

let serialQueue = DispatchQueue(label: "dev.goyes.serial")

print("Antes")
serialQueue.sync {
  print("Dentro del closure")
}
print("Después")

// Salida garantizada en este orden:
// Antes
// Dentro del closure
// Después
Enter fullscreen mode Exit fullscreen mode

En una cola serial, sync se combina con dos propiedades ya cubiertas: el FIFO (las tareas empiezan en el orden en que se despachan) y la exclusión mutua de una cola serial (solo una tarea corre a la vez). El resultado es que sync no solo espera a que termine el closure despachado — espera a que termine después de que la cola haya procesado todo lo que estaba encolado antes.

El error más común: deadlock por reentrada

Llamar sync sobre la misma cola serial en la que el código ya se está ejecutando produce un deadlock.

let serialQueue = DispatchQueue(label: "dev.goyes.serial")

serialQueue.sync {
  print("Tarea A")
  serialQueue.sync { // ⚠️ Deadlock
    print("Tarea B")
  }
}
Enter fullscreen mode Exit fullscreen mode

La tarea A está corriendo dentro de la cola serial y bloquea ese hilo esperando a que la tarea B termine. Pero la tarea B no puede empezar: la cola es serial, solo procesa una tarea a la vez, y esa tarea (A) todavía no ha terminado — está esperando a B. Ninguna de las dos puede avanzar.

La versión real de este bug: DispatchQueue.main.sync

DispatchQueue.main es, para este propósito, una cola serial cuyo único hilo de ejecución es el hilo principal. Cualquier código que ya esté corriendo en el hilo principal — un método del ciclo de vida de UIKit como viewDidLoad(), una acción de botón, cualquier callback que llegue por el hilo principal — está, en términos de la cola, en la misma situación que la tarea A del ejemplo anterior.

class SomeViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()
    DispatchQueue.main.sync { // ⚠️ Deadlock
      print("Esto nunca se imprime")
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

viewDidLoad() corre en el hilo principal. Al llamar DispatchQueue.main.sync, ese hilo se bloquea esperando a que el closure se ejecute, pero el único hilo donde la cola main puede ejecutar algo es justamente el que está bloqueado. El resultado es el mismo deadlock del ejemplo anterior, solo que disfrazado: no hace falta anidar sync explícitamente para reproducirlo, basta con llamar DispatchQueue.main.sync desde cualquier punto que ya esté corriendo en el hilo principal.

Usar sync como mecanismo de sincronización

La combinación de FIFO + exclusión mutua + bloqueo del llamador permite usar una cola serial para proteger el acceso a un recurso compartido, sin locks explícitos.

final class Cache {
  private let queue = DispatchQueue(label: "dev.goyes.cache")
  private var storage: [String: Data] = [:]

  func write(_ value: Data, forKey key: String) {
    queue.async {
      self.storage[key] = value
    }
  }

  func read(forKey key: String) -> Data? {
    queue.sync {
      storage[key]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

write despacha con async: no bloquea al llamador, solo encola la escritura. read despacha con sync: bloquea al llamador hasta que la lectura se ejecute, y por el FIFO de la cola, esa lectura solo corre después de que todas las escrituras encoladas antes ya se aplicaron. El resultado es lectura siempre consistente con el estado más reciente que se haya encolado, sin declarar ningún lock.

El costo de este enfoque es que las lecturas tampoco corren en paralelo entre sí — la cola es serial, así que dos llamados a read() desde hilos distintos también se serializan, aunque leer no modifique el estado y en principio no haya conflicto entre ellas. Ese costo es el que un DispatchQueue concurrente con DispatchBarrier evita, a cambio de más complejidad.

Lo que viene

Falta ver qué cambia cuando, sobre la misma cola serial, se despacha trabajo con async en lugar de sync. Eso es lo que cubre el siguiente artículo.


Bibliografía

Top comments (0)