How do Operation dependencies, cancellation and states interact, and what happens to a dependent operation when its dependency is cancelled?

10.11.4 · L3 · Interview

Predict first

What is the order, and does a dependent of a cancelled operation run?

import Foundation

let q = OperationQueue(); q.maxConcurrentOperationCount = 4
let lock = NSLock()
nonisolated(unsafe) var log: [String] = []
func add(_ s: String) { lock.lock(); log.append(s); lock.unlock() }
let a = BlockOperation { Thread.sleep(forTimeInterval: 0.1); add("A") }
let b = BlockOperation { add("B") }
let c = BlockOperation { add("C") }
b.addDependency(a); c.addDependency(b)
q.addOperations([c, b, a], waitUntilFinished: true)
print(log.joined(separator: ","))

let q2 = OperationQueue(); q2.maxConcurrentOperationCount = 1; q2.isSuspended = true
nonisolated(unsafe) var ran: [String] = []
let x = BlockOperation { ran.append("x") }
let y = BlockOperation { ran.append("y") }
y.addDependency(x)
q2.addOperations([x, y], waitUntilFinished: false)
x.cancel()
q2.isSuspended = false
q2.waitUntilAllOperationsAreFinished()
print("ran:", ran, "x cancelled:", x.isCancelled, "x finished:", x.isFinished, "y finished:", y.isFinished)
  • A,B,C / ran: ["y"] x cancelled: true x finished: true y finished: true
  • C,B,A / ran: [] x cancelled: true x finished: true y finished: false
  • A,B,C / ran: ["x", "y"] x cancelled: true x finished: true y finished: true

Mental model. Cancelling a step in a recipe does not cancel the next step. The cook still moves on, and must look at the pan to see that the earlier step never happened.

Dependencies are a directed graph of prerequisites: an operation becomes ready only when all its dependencies have finished, regardless of the order you add them. In the check the operations are added as C, B, A but run A, B, C because B depends on A and C on B, and no cycles are allowed (a cycle means none of them ever becomes ready). The queue’s maxConcurrentOperationCount still applies on top; 1 makes it serial.

The surprise is cancellation. Cancelling an operation does not remove it or its dependents: the cancelled operation is still “started” by the queue, skips its work (a block operation checks the flag before running the block), and moves to isFinished == true. Because a dependency that finished (cancelled or not) satisfies its dependents, the dependent does run: in the check x is cancelled before it starts, x.isFinished is true, and y still runs. If y must not run after a failed prerequisite, check dependencies.contains { $0.isCancelled } at the top of y (and cancel it), or share a result object and read it. For long operations check isCancelled periodically; a custom asynchronous Operation must manage isExecuting and isFinished with the right KVO notifications, which is why most teams wrap async work with a task and a continuation instead (Q291).

Dependencies order the work; a cancelled prerequisite still finishes, so its dependent runs.

Common traps

  • Assuming a cancelled dependency cancels its dependents. It only finishes; the dependents run.
  • Creating a dependency cycle. No operation in the cycle ever becomes ready, and nothing reports it.

They might follow up with

  • How do you pass data between operations? A shared result holder, or reading from the dependency in the dependent’s body.
  • Can I add a dependency after the operation started? No: it only affects operations that have not started.

Related questions

  • 10.11.5 How does quality of service propagate between queues and blocks, and why does it matter for priority…
  • 10.11.2 What does DispatchQueue.concurrentPerform do, when is it better than a loop of async calls, and how do you…
  • 10.8.1 In what order do task group results arrive, and how do you limit how many children run at once?
  • 10.2.1 When do you use OperationQueue instead of GCD, and how do dependencies, state and cancellation work?
  • 10.1.2 How do barriers, groups, semaphores and dispatch sources coordinate work?

← How does a DispatchSourceTimer differ from Timer, and what are the…   How does quality of service propagate between queues and blocks, and… →