iosNudge

What is the difference between GCD and OperationQueue?

MediumConcurrency 2 min read

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, upload

Cancelling:

queue.cancelAllOperations()

Cancellation is a flag. Running operations must check isCancelled themselves.

Side-by-side

GCDOperationQueue
LevelLow (C API)High (Objective-C classes)
Unit of workClosureOperation object
DependenciesManual (groups, nesting)Built in: addDependency
CancelDispatchWorkItem.cancel() (only before start)cancel(), cancelAllOperations()
Limit concurrencySemaphores (awkward)maxConcurrentOperationCount
Observe stateNoKVO on isExecuting, isFinished
PriorityQoSqueuePriority + qualityOfService
OverheadVery smallA bit more (objects, KVO)
Reuse logicFunctionsOperation 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.