DEV Community

GoyesDev
GoyesDev

Posted on

[GCD] Sincronización de hilos con DispatchBarrier

En el artículo sobre DQ Concurrente: ejecución síncrona quedó pendiente un problema: en una cola concurrente, sync no implica exclusión mutua. Una lectura despachada con sync puede correr al mismo tiempo, en un hilo distinto, que una escritura despachada con async, porque nada en la cola serializa el acceso entre tareas independientes. DispatchBarrier resuelve exactamente ese problema, sin renunciar del todo al paralelismo de una cola concurrente.

Qué hace una barrera

Una tarea despachada con la bandera .barrier sobre una cola concurrente se ejecuta con exclusión mutua: mientras corre, ninguna otra tarea de esa misma cola corre en paralelo con ella. Además, todas las tareas despachadas antes de la barrera tienen que terminar antes de que la barrera empiece, y todas las tareas despachadas después tienen que esperar a que la barrera termine. Efectivamente, la cola se comporta como serial mientras la barrera está en ejecución, y vuelve a comportarse como concurrente en cuanto termina.

Cómo despachar una tarea con .barrier

Tanto sync como async aceptan la bandera .barrier a través del parámetro flags:.

El siguiente fragmento despacha una tarea con async(flags: .barrier) sobre una cola concurrente:

let queue = DispatchQueue(label: "dev.goyes.gcd.barrier", attributes: .concurrent)

queue.async(flags: .barrier) {
  print("Corriendo en exclusión mutua respecto al resto de la cola")
}
Enter fullscreen mode Exit fullscreen mode

No tiene sentido aplicar .barrier sobre una cola serial: una cola serial ya ejecuta una tarea a la vez, así que la exclusión mutua que ofrece la barrera ya existe de por sí.

Orden garantizado alrededor de la barrera

El fragmento de abajo despacha dos tareas normales, luego una con .barrier, y luego una tarea normal más, todas sobre la misma cola concurrente:

let queue = DispatchQueue(label: "dev.goyes.gcd.barrier-order", attributes: .concurrent)
var result: [Int] = []
let group = DispatchGroup()

for i in 1...2 {
  group.enter()
  queue.async {
    Thread.sleep(forTimeInterval: 0.3)
    result.append(i)
    group.leave()
  }
}

group.enter()
queue.async(flags: .barrier) {
  result.append(3)
  group.leave()
}

group.enter()
queue.async {
  Thread.sleep(forTimeInterval: 0.1)
  result.append(4)
  group.leave()
}

group.wait()

// result empieza con 1 y 2 en algún orden, sigue con 3, y termina con 4
Enter fullscreen mode Exit fullscreen mode

El orden entre 1 y 2 no está garantizado entre sí (son dos tareas normales corriendo en paralelo), pero ambas terminan antes de que 3 (la barrera) pueda empezar. 4, despachada después de la barrera, no corre hasta que 3 termina, así que siempre queda al final.

Usar una barrera para proteger lecturas y escrituras

El fragmento siguiente retoma la clase UnsafeCache del artículo sobre DQ Concurrente: ejecución síncrona, protegida ahora con una barrera en la escritura:

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

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

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

read sigue despachando con sync, sin ninguna bandera especial: varias lecturas pueden seguir corriendo en paralelo entre sí, porque leer no modifica storage. write despacha con async(flags: .barrier): mientras una escritura está en curso, ninguna lectura ni ninguna otra escritura corre al mismo tiempo, así que ya no hay forma de leer un valor a medias ni de perder una escritura por una carrera de datos. Comparado con la versión completamente serial del primer Cache (dos artículos atrás), esta sigue permitiendo que varias lecturas corran en paralelo cuando no hay ninguna escritura de por medio, algo que la versión serial no podía ofrecer.

Cuándo no usar una barrera

Una barrera no debería aplicarse sobre una cola global (DispatchQueue.global(qos:)). Las colas globales las administra el sistema y las comparte todo el proceso (y potencialmente otros procesos), así que despachar una barrera ahí bloquea trabajo de otras partes de la app que no tienen ninguna relación con lo que la barrera protege. Para usar una barrera, lo correcto es crear una cola concurrente propia, dedicada al recurso que se quiere proteger, como en el ejemplo de Cache.

Lo que viene

Falta ver DispatchSemaphore, otra herramienta para limitar cuántas tareas acceden a un recurso al mismo tiempo, mostrado en un artículo posterior.


Bibliografía

Top comments (0)