Skip to main content

On This Page

GCD DispatchWorkItem Cancellation: Cooperative Cancellation in Swift

2 min read
Share

These articles are AI-generated summaries. Please check the original sources for full details.

Cancelando un DispatchWorkItem

DispatchWorkItem cancellation is cooperative, not preemptive. The cancel() method sets a boolean flag but cannot stop code already running.

Why This Matters

In Grand Central Dispatch (GCD) on iOS/macOS, developers often assume cancel() abruptly terminates tasks, but it only marks a flag. This misalignment between expected and actual behavior can lead to resource leaks, unresponsive UIs, or infinite loops if the executing block never checks the cancellation state.

Key Insights

  • Cancel() before execution: If a DispatchWorkItem is cancelled before it is dispatched, the closure never runs. Example: call workItem.cancel() then queue.async(execute: workItem) results in the block never executing.
  • Cancel() during execution: cancel() merely sets isCancelled to true; the onus is on the block to read isCancelled and decide to exit early. In the article’s loop example, the block checks isCancelled at each iteration, stopping after the current iteration completes.
  • wait() unaffected by cancellation: Even after cancel(), wait() blocks until the block finishes naturally or never starts—if cancelled before execution, wait() returns immediately; if during, it waits for the block to terminate.
  • Variable capture pattern for isCancelled: The DispatchWorkItem reference must be stored as an optional variable before the closure to allow the closure to check workItem?.isCancelled—a common Swift pattern for recursive or self-referencing closures.

Working Examples

A DispatchWorkItem cancelled before being dispatched never executes the closure.

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

A DispatchWorkItem that checks isCancelled on each iteration. After 1.2 seconds, cancel() is called; the loop stops on the next check after the current iteration completes.

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()

Practical Applications

  • Background database cleanup loops: Check isCancelled at loop iterations to allow graceful exit when user navigates away or the app enters background, preventing wasted work.
  • Network request wrapping: Use DispatchWorkItem for timeout-based cancellation. Pitfall: If the block does not check isCancelled, the network call or its callback may continue executing even after logical cancellation, leading to stale UI updates.

References:

Continue reading

Next article

Has AI Changed the Joy of Building? A Developer Reflects on Learning, Struggle, and Satisfaction

Related Content