7 min read
GCD vs Operation Queues

Swift Concurrency Explained: GCD and Operation Queues

Concurrency is a fundamental concept in iOS development that allows applications to perform multiple tasks simultaneously, improving performance and responsiveness. Two primary mechanisms for achieving concurrency in iOS are Grand Central Dispatch (GCD) and Operation Queues.

What Is Concurrency?

Concurrency means apps can run multiple tasks or processes, regardless of each other’s life cycle. This improves the overall performance and responsiveness of an app, since it allows multiple operations to run at the same time, whether they are in parallel or not.

Understanding Threads

A thread is the context in which operations are executed. We can think of each thread as a small, independent worker who has a single task to execute, and will execute it independently.

While we can manually create threads using simple code, this approach is generally not recommended and should only be used for very specific, low-level tasks. Using abstractions like Grand Central Dispatch is safer, simpler, and generally more advisable.

Grand Central Dispatch (GCD)

Grand Central Dispatch is a low-level C-based API provided by Apple for managing and scheduling tasks concurrently. It is highly efficient and widely used for handling concurrency in iOS apps. GCD uses a queue-based model where tasks are placed in queues and executed asynchronously.

Key Concepts of GCD

Dispatch Queues

Queues are essentially lists of tasks that need to be executed in a specific order. There are different types of queues in GCD:

Queue TypeDescriptionExecution Style
Serial QueueExecutes tasks one at a time, in the order they are addedSequential
Concurrent QueueExecutes tasks concurrently, allowing multiple tasks to run simultaneouslyParallel (when possible)

Here’s how to create and use these queues:

// Serial Queue
let serialQueue = DispatchQueue(label: "mySerialQueue")
serialQueue.async {
    print("1 started")
    print("1 finished")
}
serialQueue.async {
    print("2 started")
    print("2 finished")
}

// Concurrent Queue
let concurrentQueue = DispatchQueue(label: "myConcurrentQueue", attributes: .concurrent)
concurrentQueue.async {
    print("3 started")
    print("3 finished")
}
concurrentQueue.async {
    print("4 started")
    print("4 finished")
}

Global Queues and Quality of Service (QoS)

Global Queues automatically let us choose a Quality of Service level, which refers to the priority in which they get executed.

QoS LevelPriorityUse Case
.userInteractiveHighestTasks requiring immediate results, triggered by user actions
.userInitiatedHighTasks that should complete quickly for a good user experience
.defaultMediumDefault priority, runs after high-priority but before low-priority
.utilityLowTasks not actively tracked by the user (downloading assets)
.backgroundLowestLong-running tasks that don’t interfere with user experience

Example usage:

DispatchQueue.global(qos: .userInteractive).async {
    print("My userInteractive task")
}

DispatchQueue.global(qos: .background).async {
    print("My background task")
}

The Main Queue

The main Queue is a special case, since it’s the only one we rely on to update our UI. If we use it for time-consuming tasks, the UI will freeze.

DispatchQueue.global(qos: .background).async {
    // Perform background work
    
    DispatchQueue.main.async {
        // Update UI here
    }
}

Operation Queue

Operation Queue is a higher-level abstraction built on top of GCD. It provides a more object-oriented approach to managing concurrent tasks. Operations are represented as instances of the Operation class, which you can customize and add to operation queues.

Key Concepts of Operation Queue

FeatureDescription
OperationEncapsulated task that can be customized and controlled
DependenciesAbility to define operation dependencies, controlling execution order
CancellationOperations can be canceled before or during execution
Operation StateOperations can be in various states (ready, executing, finished)
Maximum ConcurrencyControl the number of operations that can execute simultaneously

You can configure an OperationQueue in several ways:

let operationQueue = OperationQueue()

// Define how many concurrent operations (1 makes it sequential)
operationQueue.maxConcurrentOperationCount = 1;

// Define the QoS
operationQueue.qualityOfService = .background

// Define a name for identification
operationQueue.name = "myQueue"

// Wait for completion
operationQueue.waitUntilAllOperationsAreFinished()

// Cancel all operations
operationQueue.cancelAllOperations()

BlockOperation Example

let operationQueue = OperationQueue()
let operation1 = BlockOperation {
    print("Task 1 started")
}
let operation2 = BlockOperation {
    print("Task 2 started")
}
operationQueue.addOperations([operation1, operation2], waitUntilFinished: false)

Custom Operation Example

class ImageDownloadOperation: Operation {
    let imageURL: URL
    var image: UIImage?
    
    init(url: URL) {
        self.imageURL = url
        super.init()
    }
    
    override func main() {
        if isCancelled {
            return
        }
        
        // Download the image
        if let imageData = try? Data(contentsOf: imageURL) {
            image = UIImage(data: imageData)
        }
    }
}

// Create an operation queue
let imageDownloadQueue = OperationQueue()

// Create and add operations
for url in imageUrls {
    let operation = ImageDownloadOperation(url: url)
    imageDownloadQueue.addOperation(operation)
}

GCD vs. Operation Queue: Comparison

FeatureGrand Central Dispatch (GCD)Operation Queue
Abstraction LevelLow-level C-based APIHigher-level Objective-C/Swift API
ComplexitySimpler for basic tasksMore complex but more powerful
Task EncapsulationUses closures for tasksUses Operation objects
DependenciesLimited supportRobust dependency management
CancellationLimited supportFull support with cancellation states
Priority ManagementSupports QoS levelsSupports QoS and additional priority control
Memory OverheadLowerHigher due to object creation
Use CaseSimple, isolated asynchronous tasksComplex workflows with interdependent tasks

When to Choose Which?

  • Use GCD when:

    • You need a lightweight solution for simple tasks
    • Performance is critical
    • Tasks are independent and don’t need to be canceled
    • You need a simpler API with less boilerplate code
  • Use Operation Queue when:

    • You need fine-grained control over task execution
    • Tasks have dependencies on each other
    • You need robust cancellation support
    • You want to reuse encapsulated operations
    • You need to monitor operation state and progress

Best Practices for Concurrency in iOS

  1. Avoid Blocking the Main Thread: Always perform computationally intensive or time-consuming tasks off the main thread to keep your UI responsive.

  2. Choose the Right QoS Level: Select appropriate Quality of Service levels based on the priority and importance of your tasks.

  3. Consider Thread Safety: When working with shared resources across multiple threads, implement proper synchronization techniques to avoid race conditions.

  4. Error Handling: Implement robust error handling in your asynchronous operations to gracefully handle failures.

  5. Minimize Context Switching: Too many thread context switches can degrade performance, so design your concurrency implementation to minimize unnecessary switching.

Conclusion

Understanding and correctly implementing concurrency is crucial for developing responsive and efficient iOS applications. Grand Central Dispatch offers a lightweight, low-level approach to concurrency that’s suitable for simpler tasks, while Operation Queues provide a more object-oriented framework with greater control over task execution and dependencies.

By mastering these concurrency tools, you’ll be able to build iOS applications that make optimal use of system resources while delivering an exceptional user experience.