When do you use OperationQueue instead of GCD, and how do dependencies, state and cancellation work?
Predict first
Operations are added in the order C, B, A, with C depending on B and B on A. In which order do they run?
import Foundation
let oq = OperationQueue()
oq.maxConcurrentOperationCount = 4
nonisolated(unsafe) var order: [String] = []
let lock = NSLock()
func op(_ name: String) -> BlockOperation { BlockOperation { lock.lock(); order.append(name); lock.unlock() } }
let a = op("A"), b = op("B"), c = op("C")
c.addDependency(b); b.addDependency(a)
oq.addOperations([c, b, a], waitUntilFinished: true)
print(order)
- ["C", "B", "A"]
- ["A", "B", "C"]
- Any order, they are concurrent
Predict first
An operation is cancelled while its queue is suspended. What does it report before and after the queue runs?
import Foundation
let q = OperationQueue()
q.isSuspended = true
let c = BlockOperation { }
q.addOperation(c)
c.cancel()
print(c.isCancelled, c.isFinished, q.operationCount)
q.isSuspended = false
q.waitUntilAllOperationsAreFinished()
print(c.isFinished, q.operationCount)
- true false 1, then true 0
- true true 0, then true 0
- false false 1, then true 0
Predict first
What is the default maxConcurrentOperationCount?
import Foundation
print(OperationQueue().maxConcurrentOperationCount, OperationQueue.defaultMaxConcurrentOperationCount)
- -1 -1
- 1 1
- 4 4
Mental model. GCD hands out tasks on slips of paper. OperationQueue gives each task a job card with a status, a list of prerequisites, and a “stop” stamp.
An Operation (usually a BlockOperation or your subclass) has a state machine: ready, executing, finished, with isCancelled as a flag. An OperationQueue runs operations once they are ready, which means all of their dependencies have finished (op.addDependency(other), even across queues). That gives you ordering without nested callbacks: C depends on B depends on A runs A, B, C however they were added. maxConcurrentOperationCount limits parallelism (the default is OperationQueue.defaultMaxConcurrentOperationCount, a system-chosen value, reported as −1), setting it to 1 makes the queue serial, qualityOfService sets the QoS, and isSuspended pauses starting new work. You can observe state with key-value observing, add completion blocks, and put a barrier with addBarrierBlock. Never call waitUntilAllOperationsAreFinished from the main thread or an async context.
Cancellation is cooperative, as everywhere: cancel() sets isCancelled and, if the operation has not started, the queue will skip its work and mark it finished when it reaches it; a running operation must check isCancelled at safe points and stop. A cancelled operation that is waiting still counts in operationCount until the queue processes it, and dependants of a cancelled operation still run (a cancelled dependency still counts as finished), so check it in dependants. Asynchronous operations (subclasses wrapping a callback) must manage isExecuting and isFinished manually with KVO notifications, which is easy to get wrong, and is the main reason to prefer async/await for new code. Choose OperationQueue when you need dependencies, a concurrency cap with cancellation, or reusable units with state; choose GCD for simple fire-and-forget and serialisation; choose Swift concurrency (task groups, async let) for new work.
An operation is not ready until all its dependencies have finished, so the dependency chain fixes the order.
Cancelling sets the flag but the queue only completes the operation when it processes it, so the count drops after the queue resumes.
The default asks the system to decide, and it is reported as the constant −1.
| GCD | OperationQueue | Swift concurrency | |
|---|---|---|---|
| Unit of work | a block | an Operation object | a Task or async function |
| Dependencies | groups, manual | addDependency | structure: await, task groups |
| Cancellation | work items, manual | cancel(), isCancelled | cooperative, propagates to children |
| Limit concurrency | semaphore | maxConcurrentOperationCount | task group of bounded size |
Common traps
- Assuming a cancelled operation’s dependants will not run. They run when the dependency finishes, cancelled or not.
- Writing an asynchronous
Operationwithout the KVO state notifications. The queue never sees it finish, or finishes it too early.
They might follow up with
- How do you make an
OperationQueueserial?maxConcurrentOperationCount = 1. - What does
isSuspendeddo? It stops the queue from starting new operations; running ones continue.