What is the difference between GCD and OperationQueue?
TL;DR
GCD (Grand Central Dispatch) runs closures on queues. It is light, fast and simple.
OperationQueue runs Operation objects. It is built on top of GCD and adds dependencies, cancellation, max concurrency and state you can observe.
For new code, Swift concurrency (async/await, task groups) usually replaces both. You still need to know them for older code and interviews.
GCD in one example
DispatchQueue.global(qos: .userInitiated).async {
let image = loadBigImage()
DispatchQueue.main.async {
imageView.image = image // UI on the main queue
}
}Useful GCD tools:
let group = DispatchGroup()
for url in urls {
group.enter()
download(url) { _ in group.leave() }
}
group.notify(queue: .main) { print("All done") }
let serial = DispatchQueue(label: "com.app.cache") // serial by default
serial.asyncAfter(deadline: .now() + 1) { print("Later") }OperationQueue in one example
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2
let download = BlockOperation { print("download") }
let resize = BlockOperation { print("resize") }
let upload = BlockOperation { print("upload") }
resize.addDependency(download)
upload.addDependency(resize)
queue.addOperations([download, resize, upload], waitUntilFinished: false)
// Always prints: download, resize, uploadCancelling:
queue.cancelAllOperations()Cancellation is a flag. Running operations must check isCancelled themselves.
Side-by-side
| GCD | OperationQueue | |
|---|---|---|
| Level | Low (C API) | High (Objective-C classes) |
| Unit of work | Closure | Operation object |
| Dependencies | Manual (groups, nesting) | Built in: addDependency |
| Cancel | DispatchWorkItem.cancel() (only before start) | cancel(), cancelAllOperations() |
| Limit concurrency | Semaphores (awkward) | maxConcurrentOperationCount |
| Observe state | No | KVO on isExecuting, isFinished |
| Priority | QoS | queuePriority + qualityOfService |
| Overhead | Very small | A bit more (objects, KVO) |
| Reuse logic | Functions | Operation subclasses |
When to use which (classic answer)
GCD for:
- Simple background work and hopping back to the main queue.
- Serial queues to protect state.
- One-off delays with
asyncAfter.
OperationQueue for:
- Work with clear steps that depend on each other.
- Work that the user can cancel, like an image upload queue.
- Limiting parallel work, like "at most 3 downloads at once".
Async operations (a common follow-up)
By default, an Operation is finished when main() returns. For async work (like a network call), you subclass and manage the state yourself:
final class AsyncOperation: Operation, @unchecked Sendable {
private let lock = NSLock()
private var _executing = false
private var _finished = false
override var isAsynchronous: Bool { true }
override var isExecuting: Bool { lock.withLock { _executing } }
override var isFinished: Bool { lock.withLock { _finished } }
override func start() {
guard !isCancelled else { finish(); return }
willChangeValue(forKey: "isExecuting")
lock.withLock { _executing = true }
didChangeValue(forKey: "isExecuting")
work { [weak self] in self?.finish() }
}
func work(completion: @escaping () -> Void) { completion() } // override this
func finish() {
willChangeValue(forKey: "isExecuting")
willChangeValue(forKey: "isFinished")
lock.withLock { _executing = false; _finished = true }
didChangeValue(forKey: "isExecuting")
didChangeValue(forKey: "isFinished")
}
}The KVO notifications are required. Without them, the queue never knows the operation finished.
The modern way
let results = try await withThrowingTaskGroup(of: Data.self) { group in
for url in urls {
group.addTask { try await download(url) }
}
return try await group.reduce(into: []) { $0.append($1) }
}Task groups give structured cancellation and errors with less code. Actors replace serial queues for protecting state.
Common follow-up questions
- Is OperationQueue built on GCD? Yes. It uses dispatch queues internally.
- What is
OperationQueue.main? A queue that runs operations on the main thread. It is serial. - What is a
DispatchWorkItem? A GCD closure wrapper you can cancel before it starts, or wait on. - Can you mix GCD and async/await? Yes, but avoid blocking calls like
semaphore.wait()inside async code. It can starve the cooperative thread pool. - What is QoS? Quality of Service:
.userInteractive,.userInitiated,.utility,.background. It tells the system how urgent the work is.