DEV Community

GoyesDev
GoyesDev

Posted on

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

Despachar un closure con async lo encola y retorna de inmediato. El hilo que hizo el llamado no espera a que el closure se ejecute. Sigue corriendo el código que viene después sin bloquearse.

Consideremos el siguiente fragmento de código, donde se despacha un bloque con async sobre una cola serial:

let serialQueue = DispatchQueue(label: "dev.goyes.gcd.serial")
var result: [Int] = []
let group = DispatchGroup()

result.append(1)

group.enter()
serialQueue.async {
  Thread.sleep(forTimeInterval: 1)
  result.append(2)
  group.leave()
}

result.append(3)

group.wait()

// result == [1, 3, 2]
Enter fullscreen mode Exit fullscreen mode

DispatchGroup se cubre en detalle en un artículo posterior. Aquí solo hace falta saber que group.wait() bloquea hasta que todo lo que entró con group.enter() haya salido con group.leave(), y se usa únicamente para poder comprobar el resultado al final, no como parte de lo que se está demostrando.

Lo único garantizado en este código es que result.append(1) ocurre antes que cualquiera de los otros dos: es código síncrono, anterior al llamado a async. El orden entre result.append(3) y result.append(2) no está garantizado por la API: async retorna de inmediato, así que result.append(3) casi siempre se ejecuta primero, y por eso el resultado es [1, 3, 2] y no [1, 2, 3].

La cola sigue siendo serial: el orden entre tareas sí está garantizado

async cambia si el llamador espera, no cambia cómo procesa la cola las tareas. Una cola serial sigue ejecutando una tarea a la vez, en el orden en que se despacharon (FIFO), sin importar si se despacharon con sync o async.

Consideremos el siguiente fragmento de código, donde se despachan tres bloques con async sobre la misma cola serial:

let serialQueue = DispatchQueue(label: "dev.goyes.gcd.serial")
var result: [Int] = []
let group = DispatchGroup()

result.append(1)

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

result.append(5)

group.wait()

// result == [1, 5, 2, 3, 4]
Enter fullscreen mode Exit fullscreen mode

result.append(5) se ejecuta de inmediato, antes de que cualquiera de las tres tareas asíncronas alcance a correr (cada una duerme un segundo completo antes de anotar su número). Pero el orden entre 2, 3 y 4 sí está garantizado por el FIFO de la cola serial: corren una a la vez, en el orden en que se despacharon, sin importar que ninguna haya bloqueado al llamador. Por eso no es necesario sincronizar la escritura sobre result entre las tres tareas: al ser una cola serial, nunca corren en paralelo entre sí, así que no se accede a result de forma concurrente en ningún momento.

async reentrante no produce deadlock

A diferencia de sync, llamar async sobre la misma cola serial en la que el código ya se está ejecutando es seguro. async nunca bloquea, así que solo agrega el closure al final de la cola y continúa.

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

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

serialQueue.async {
  print("Tarea A")
  serialQueue.async { // Seguro, no hay deadlock
    print("Tarea B")
  }
  print("A sigue ejecutando")
}

// Tarea A
// A sigue ejecutando
// Tarea B
Enter fullscreen mode Exit fullscreen mode

La tarea A termina de ejecutarse por completo (incluyendo el print después del async anidado) antes de que la cola pase a la tarea B, porque B se encoló al final, detrás de lo que faltaba de A.

Por qué Cache.write usa async

En el artículo anterior, write(_:forKey:) despacha con async precisamente por esto: quien escribe en la caché no necesita esperar a que la escritura se aplique para seguir ejecutando. Lo único que importa es que, cuando se llegue a leer con sync, todas las escrituras encoladas antes ya se hayan procesado. Eso lo garantiza el FIFO de la cola serial, no el hecho de que el llamador haya esperado.

Lo que viene

Falta ver qué cambia cuando la cola, en lugar de serial, es concurrente: varias tareas pueden correr al mismo tiempo, y el FIFO deja de implicar "una a la vez". Eso es lo que cubren los siguientes dos artículos.


Bibliografía

Top comments (0)