La API es la misma que en una cola serial: sync bloquea al llamador hasta que el closure despachado termina. Lo que cambia es qué hace la cola con el resto de su trabajo mientras tanto.
let concurrentQueue = DispatchQueue(label: "dev.goyes.concurrente", attributes: .concurrent)
print("Antes")
concurrentQueue.sync {
print("Dentro del closure")
}
print("Después")
// Salida garantizada en este orden:
// Antes
// Dentro del closure
// Después
Para una sola tarea despachada con sync, el comportamiento observado es idéntico al de una cola serial: el llamador espera, y el orden de las tres líneas está garantizado. La diferencia aparece cuando hay más tareas además de la que se está esperando.
sync bloquea por esa tarea, no por la cola
En una cola concurrente, varias tareas pueden estar corriendo en hilos distintos al mismo tiempo. sync bloquea al llamador hasta que su tarea termine — no detiene ni espera a las demás tareas que la cola esté procesando en paralelo.
let concurrentQueue = DispatchQueue(label: "dev.goyes.concurrente", attributes: .concurrent)
for i in 1...3 {
concurrentQueue.async {
print("Tarea \(i) empieza, hilo: \(Thread.current)")
Thread.sleep(forTimeInterval: 1)
print("Tarea \(i) termina")
}
}
concurrentQueue.sync {
print("Tarea de sincronización")
}
Las tres tareas despachadas con async se despachan, en orden de código, antes que la "Tarea de sincronización". El sync final espera a que su propio closure termine, pero no espera a que las tres tareas anteriores terminen — esas siguen corriendo en paralelo, en sus propios hilos, independientemente de lo que haga el llamador.
El orden de entrega FIFO no garantiza el orden real de inicio
Al correr exactamente este código, una salida real fue:
Tarea de sincronización
Tarea 2 empieza, hilo: <NSThread: 0x104b29bc0>{number = 7, name = (null)}
Tarea 3 empieza, hilo: <NSThread: 0x1048e06c0>{number = 3, name = (null)}
Tarea 1 empieza, hilo: <NSThread: 0x1048e0b80>{number = 5, name = (null)}
"Tarea de sincronización" — despachada después de las tres — imprimió primero. Y entre las tres tareas async, el orden tampoco fue 1, 2, 3, sino 2, 3, 1.
Esto no contradice el FIFO de la cola: el FIFO garantiza el orden en que GCD entrega los closures para ejecución, no el orden real en que cada hilo ejecuta su primera instrucción. Una vez entregado un closure a un hilo, es el scheduler del sistema operativo quien decide cuándo ese hilo corre — no GCD. Y sync, a diferencia de async, puede reutilizar el hilo que hace el llamado en lugar de pedir uno nuevo al pool concurrente: no tiene que pagar el costo de adquisición de un hilo nuevo, que sí pagan las tres tareas despachadas con async. Por eso terminó primero pese a haberse despachado después.
El riesgo: sync no implica exclusión mutua
Como una cola concurrente no serializa el acceso, usar sync para leer un recurso no protege contra una escritura que se esté ejecutando en paralelo en otro hilo de la misma cola.
final class UnsafeCache {
private let queue = DispatchQueue(label: "dev.goyes.cache-concurrente", attributes: .concurrent)
private var storage: [String: Data] = [:]
func write(_ value: Data, forKey key: String) {
queue.async {
self.storage[key] = value // ⚠️ Puede correr al mismo tiempo que una lectura
}
}
func read(forKey key: String) -> Data? {
queue.sync {
storage[key]
}
}
}
A diferencia del Cache con cola serial del artículo anterior, aquí read y write pueden ejecutarse en paralelo, en hilos distintos, sobre el mismo diccionario — una carrera de datos. El FIFO de la cola solo garantiza el orden en que las tareas se entregan para ejecución, no que una tarea espere a que la anterior termine antes de arrancar, y mucho menos que se ejecuten en exclusión mutua entre sí. Proteger el acceso en este escenario requiere DispatchBarrier, cubierto en un artículo posterior.
Lo que viene
Falta ver el mismo tipo de cola concurrente, pero despachando con async en lugar de sync. Ese es el siguiente artículo.
Top comments (0)