What's the difference between GCD and OperationQueue?

GCD is Apple's low-level API for scheduling work on dispatch queues. `OperationQueue` is a higher-level Foundation API that represents work as `Operation` objects with dependencies, cancellation, priorities, and concurrency limits.

Answer

The core idea

GCD (DispatchQueue) is Apple's low-level API for scheduling work on serial or concurrent dispatch queues. OperationQueue is a higher-level Foundation abstraction that schedules Operation objects, typically using GCD underneath. Operations add capabilities such as dependencies, cancellation, priorities, and concurrency limits.

The short interview answer is: use GCD for simple "run this later" work, and OperationQueue when you need a structured graph of cancelable tasks.

1. What each one optimizes for

GCD is about queues and Quality of Service (QoS). You submit a block, choose a queue and QoS, and optionally hop back to the main queue:

swift 5.2
DispatchQueue.global(qos: .userInitiated).async {
    let result = performHeavyTask()

    DispatchQueue.main.async {
        print(result)
    }
}

OperationQueue is about operations. You can define dependencies, cancel work, and limit how many operations execute concurrently. It still typically schedules work using GCD underneath.

underlyingQueue defaults to nil, allowing the operation queue to manage scheduling itself. Set it only when you want operations to execute on a specific dispatch queue alongside other work submitted to that queue.

swift 5.2
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2
queue.underlyingQueue = DispatchQueue.global(qos: .utility)

let prepare = BlockOperation {
    print("prepare")
}

let upload = BlockOperation {
    print("upload")
}

upload.addDependency(prepare)

queue.addOperations([prepare, upload], waitUntilFinished: false)

That dependency edge is the clearest interview difference. GCD has no built-in API for expressing "operation B depends on operation A." With Operation, you also get cancellation (cancel() / isCancelled), which naturally fits pipelines and long-running work.

You can also influence scheduling priority:

swift 5.2
upload.qualityOfService = .userInitiated

qualityOfService communicates the importance of the work to the system, helping it make scheduling and resource-allocation decisions.

queuePriority also exists. It only affects the execution order of ready operations within the same queue and is not a substitute for dependencies. Prefer qualityOfService to express importance and dependencies to express ordering. (The older threadPriority property is the one that has been deprecated in favor of qualityOfService.)

If underlyingQueue is set, that dispatch queue's QoS takes precedence over the operation queue's qualityOfService.

2. Ways to create an operation

You usually create operations in one of three ways.

Add a block directly when the work is simple and you do not need an Operation instance:

swift 5.2
queue.addOperation {
    print("one-off work")
}

Use BlockOperation when you need an Operation object for dependencies or cancellation:

swift 5.2
let prepare = BlockOperation {
    print("prepare A")
}

prepare.addExecutionBlock {
    print("prepare B")
}

A single BlockOperation can execute multiple blocks.

Subclass Operation when the task needs custom state, asynchronous completion, or repeated cancellation checks:

swift 5.2
final class UploadOperation: Operation {
    private let fileURL: URL

    init(fileURL: URL) {
        self.fileURL = fileURL
        super.init()
    }

    override func main() {
        guard !isCancelled else { return }
        upload(fileURL)
    }
}

queue.addOperation(UploadOperation(fileURL: url))

For UI work, OperationQueue.main is the operation-based equivalent of DispatchQueue.main. Use it when your surrounding code already uses operations. Otherwise, DispatchQueue.main.async is usually the simpler choice.

3. Waiting for operations to finish

Methods such as waitUntilFinished block the current thread until the operation completes.

Wait for specific operations:

swift 5.2
queue.addOperations([prepare, upload], waitUntilFinished: true)
// ⚠️ Blocks the current thread until both operations finish

upload.waitUntilFinished()
// ⚠️ Blocks the current thread until `upload` finishes

Wait until the queue becomes idle:

swift 5.2
queue.addOperation(prepare)
queue.addOperation(upload)

queue.waitUntilAllOperationsAreFinished()
// ⚠️ Blocks the current thread

Avoid calling these methods on the main thread, especially if the operations need the main queue, or you can deadlock. Prefer reacting to completion instead of blocking whenever possible.

You can react without blocking by using completionBlock or another dependent operation:

swift 5.2
upload.completionBlock = {
    print("upload finished")
}

let cleanup = BlockOperation {
    print("cleanup")
}

cleanup.addDependency(upload)
queue.addOperation(cleanup)

completionBlock runs after the operation finishes. A dependent operation is often a cleaner choice when the next step is still part of the same workflow.

4. Why it matters

Most asynchronous work only needs GCD: perform work in the background, then update the UI on the main queue.

OperationQueue becomes valuable when work forms a pipeline, when users can cancel tasks, when tasks depend on one another, or when you need to limit concurrency.

underlyingQueue highlights the layering: OperationQueue models tasks, while GCD performs the execution. Choosing OperationQueue is about gaining structure, not replacing GCD.

Interview angle

Lead with the layering: GCD schedules blocks on dispatch queues, while OperationQueue schedules Operation objects with dependencies, cancellation, and concurrency control.

Show a simple DispatchQueue.async example, then an OperationQueue example with a dependency. Mention the three ways to create operations (addOperation, BlockOperation, and subclassing Operation) and explain how to wait for completion (waitUntilFinished, waitUntilAllOperationsAreFinished, or completionBlock). Finish with the rule of thumb: use GCD for simple asynchronous work, and OperationQueue when you need dependencies, cancellation, or explicit concurrency management.