DEV Community

GoyesDev
GoyesDev

Posted on

[GCD] Cancelando un DispatchWorkItem

Un DispatchWorkItem se puede cancelar con cancel(), pero la cancelación es cooperativa, no preventiva: cancel() no interrumpe código que ya está corriendo. Lo que hace depende de en qué momento se llama, antes de que el trabajo empiece a correr o mientras ya está corriendo.

Cancelar antes de que empiece a correr

Si cancel() se llama antes de que el DispatchWorkItem empiece a ejecutarse, el bloque nunca llega a correr. GCD comprueba el estado de cancelación justo antes de invocar el bloque y, si ya está cancelado, se lo salta por completo.

Consideremos el siguiente fragmento de código, donde se cancela un DispatchWorkItem antes de despacharlo:

let queue = DispatchQueue(label: "dev.goyes.gcd.workitem-cancel")
let workItem = DispatchWorkItem {
  print("Este mensaje nunca se imprime")
}

workItem.cancel()
queue.async(execute: workItem)
Enter fullscreen mode Exit fullscreen mode

No importa que después se despache con queue.async(execute: workItem): como ya estaba cancelado antes de eso, el closure nunca se ejecuta.

Cancelar mientras ya está corriendo: cancelación cooperativa

Si cancel() se llama después de que el bloque ya empezó a correr, no lo detiene. Lo único que hace es marcar isCancelled como true. Detener el trabajo depende del bloque mismo: tiene que revisar isCancelled en algún punto de su propia ejecución y decidir terminar antes de tiempo.

El siguiente fragmento muestra un DispatchWorkItem que ejecuta un bucle y revisa isCancelled en cada iteración:

var workItem: DispatchWorkItem?

workItem = DispatchWorkItem {
  for i in 1...5 {
    if workItem?.isCancelled == true {
      print("Cancelado, deteniendo en la iteración \(i)")
      break
    }
    print("Iteración \(i)")
    Thread.sleep(forTimeInterval: 0.5)
  }
}

let queue = DispatchQueue(label: "dev.goyes.gcd.workitem-cancel")
queue.async(execute: workItem!)

Thread.sleep(forTimeInterval: 1.2)
workItem?.cancel()
workItem?.wait()
Enter fullscreen mode Exit fullscreen mode

workItem se declara como una variable opcional separada, en lugar de asignarse directamente al resultado de DispatchWorkItem { ... }, porque el bloque necesita referenciar la variable workItem para leer isCancelled, y esa variable todavía no existe mientras se está construyendo el closure. Al llamar cancel() después de 1.2 segundos, el bucle ya alcanzó a correr varias iteraciones (cada una tarda 0.5 segundos). isCancelled pasa a true en ese momento, pero la iteración que está en curso no se interrumpe: el bloque solo se entera y se detiene la próxima vez que su código revisa la propiedad, al principio de la siguiente iteración.

Si el bloque nunca revisara isCancelled, cancel() marcaría la propiedad de todos modos, pero el bucle completaría sus 5 iteraciones sin que nada lo detuviera.

wait() sigue funcionando igual con un DispatchWorkItem cancelado

cancel() no afecta el comportamiento de wait() ni de notify(queue:execute:). Si el trabajo se cancela antes de correr, wait() retorna de inmediato porque no hay nada que ejecutar. Si se cancela mientras corre, wait() sigue bloqueando hasta que el bloque termine por su cuenta.

Lo que viene

Falta ver cómo coordinar varias tareas que corren en paralelo y esperar a que todas terminen, con DispatchGroup. Eso es lo que cubre el siguiente artículo.


Bibliografía

Top comments (0)