Skip to content

Operations and Operation Queues

Operations

An Operation is an abstract class in the Foundation framework that represents a single, self-contained task or unit of work. It provides a structured way to encapsulate code, manage execution state, and handle dependencies and cancellation. Operations can be executed standalone or within an OperationQueue, making them ideal for complex, interdependent tasks in iOS apps, such as downloading files, processing data, or performing computations.

Key Features:

  • Encapsulation: Wraps a task in a reusable object.
  • State Tracking: Monitors lifecycle states (e.g., ready, executing, finished).
  • Dependencies: Allows tasks to depend on others for ordered execution.
  • Cancellation: Supports stopping tasks gracefully via cancel().
  • Priority and QoS: Uses queuePriority and qualityOfService to influence scheduling.
  • Synchronous or Asynchronous: Can be configured for either execution style.

Use Case: Downloading an image, processing it, and updating the UI, with each step as a separate operation.

Operations State

An Operation progresses through a well-defined lifecycle, tracked by key properties:

  • isReady: true when the operation is ready to execute (all dependencies satisfied). Default: true unless dependencies exist.
  • isExecuting: true when the operation is actively running. Default: false.
  • isFinished: true when the operation completes or is cancelled. Default: false.
  • isCancelled: true when cancel() is called. Default: false.
  • isAsynchronous: true if the operation runs asynchronously (custom subclasses only). Default: false.

These properties are Key-Value Observing (KVO) compliant, enabling OperationQueue to monitor state changes. In custom operations, you must manually manage isExecuting and isFinished for asynchronous tasks.

State Transitions:

  • Pending (isReady = true, isExecuting = false, isFinished = false).
  • Executing (isReady = false, isExecuting = true, isFinished = false).
  • Finished (isReady = false, isExecuting = false, isFinished = true).

Example: Observing Operation State

swift
let operation = BlockOperation {
    print("Operation running")
}
operation.completionBlock = {
    print("Operation finished: isFinished=\(operation.isFinished), isCancelled=\(operation.isCancelled)")
}
print("Initial state: isReady=\(operation.isReady), isExecuting=\(operation.isExecuting)")
operation.start()

Output:

Initial state: isReady=true, isExecuting=false
Operation running
Operation finished: isFinished=true, isCancelled=false

BlockOperation

BlockOperation is a concrete subclass of Operation that executes one or more closures (blocks) concurrently. It’s a simple way to define tasks without subclassing Operation, ideal for straightforward operations that don’t require complex state management.

Key Features:

  • Multiple Blocks: Supports adding multiple closures via addExecutionBlock(_:).
  • Automatic State Management: Handles isExecuting and isFinished internally.
  • Concurrent Execution: Multiple blocks may run concurrently on separate threads.
  • Completion Block: Supports a completionBlock for post-execution tasks.

Example: Basic BlockOperation

swift
let blockOperation = BlockOperation {
    print("Executing block 1 on \(Thread.current)")
    Thread.sleep(forTimeInterval: 1)
}
blockOperation.addExecutionBlock {
    print("Executing block 2 on \(Thread.current)")
    Thread.sleep(forTimeInterval: 0.5)
}
blockOperation.completionBlock = {
    print("Block operation completed")
}
blockOperation.start()

Output (order may vary due to concurrency):

Executing block 1 on <Thread 0x...>
Executing block 2 on <Thread 0x...>
Block operation completed

Use Case: Performing multiple quick tasks, like parsing JSON and saving data, as separate blocks within one operation.

Run Block Operation Concurrently

To run a BlockOperation concurrently, you add it to an OperationQueue with a maxConcurrentOperationCount greater than 1, or rely on its ability to execute multiple blocks concurrently when started manually. When multiple execution blocks are added, BlockOperation may run them on separate threads, depending on system resources.

Example: Concurrent BlockOperation in OperationQueue

swift
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 3

let blockOperation = BlockOperation()
for i in 1...3 {
    blockOperation.addExecutionBlock {
        print("Block \(i) started on \(Thread.current)")
        Thread.sleep(forTimeInterval: Double.random(in: 0.5...1.5))
        print("Block \(i) completed")
    }
}
blockOperation.completionBlock = {
    print("All blocks finished")
}
queue.addOperation(blockOperation)

Output (order varies):

Block 1 started on <Thread 0x...>
Block 2 started on <Thread 0x...>
Block 3 started on <Thread 0x...>
Block 2 completed
Block 1 completed
Block 3 completed
All blocks finished

Explanation:

  • Three blocks are added to a single BlockOperation.
  • The OperationQueue with maxConcurrentOperationCount = 3 allows all blocks to run concurrently.
  • Each block runs on a separate thread, managed by the queue.

Use Case: Processing multiple images in parallel within a single operation, such as resizing or applying filters.

Custom Operations

Custom operations are created by subclassing Operation to define complex tasks with custom behavior, state management, or asynchronous execution. You override methods like main() (for synchronous tasks) or start() (for asynchronous tasks) and manage KVO-compliant properties (isExecuting, isFinished).

Example: Synchronous Custom Operation

swift
class DataTransformOperation: Operation {
    private let input: [Int]
    private var output: [Int]?
    private var _executing = false
    private var _finished = false

    init(input: [Int]) {
        self.input = input
        super.init()
    }

    override var isExecuting: Bool {
        get { _executing }
        set {
            willChangeValue(forKey: "isExecuting")
            _executing = newValue
            didChangeValue(forKey: "isExecuting")
        }
    }

    override var isFinished: Bool {
        get { _finished }
        set {
            willChangeValue(forKey: "isFinished")
            _finished = newValue
            didChangeValue(forKey: "isFinished")
        }
    }

    override func main() {
        guard !isCancelled else {
            isFinished = true
            return
        }
        isExecuting = true
        print("Transforming \(input.count) items on \(Thread.current)")
        output = input.map { $0 * 2 }
        Thread.sleep(forTimeInterval: 1) // Simulate work
        if isCancelled {
            output = nil
            print("Operation cancelled")
        } else {
            print("Transformed: \(output?.prefix(5) ?? [])")
        }
        isExecuting = false
        isFinished = true
    }

    func getOutput() -> [Int]? { output }
}

Explanation:

  • Transforms an array by doubling each element.
  • Manages isExecuting and isFinished with KVO compliance.
  • Checks isCancelled to support cancellation.
  • Runs synchronously in main().

Use Case: Performing CPU-intensive tasks like data encryption or image processing with cancellation support.

Operation Queues

An OperationQueue is a high-level Foundation framework class that manages the execution of Operation objects. It schedules operations based on their dependencies, priorities, and system resources, providing a robust mechanism for concurrent or sequential task execution. Unlike GCD queues, OperationQueue excels at handling complex workflows with dependencies and cancellation.

Key Features:

  • Concurrency Control: Configures maxConcurrentOperationCount to limit simultaneous operations.
  • Dependency Management: Enforces execution order via operation dependencies.
  • Cancellation: Cancels individual or all operations.
  • Suspension: Pauses/resumes execution with isSuspended.
  • Priority Scheduling: Uses queuePriority and qualityOfService to prioritize tasks.
  • Progress Tracking: Integrates with Progress for task monitoring.

Key Properties and Methods:

  • maxConcurrentOperationCount: Maximum concurrent operations (1 for serial, defaultMaxConcurrentOperationCount for system default).
  • isSuspended: true to pause, false to resume.
  • addOperation(_:), addOperations(_:waitUntilFinished:): Add operations to the queue.
  • cancelAllOperations(): Cancels all queued and running operations.
  • waitUntilAllOperationsAreFinished(): Blocks until all operations complete (use sparingly).
  • addBarrierBlock(_:) (iOS 13+): Executes a block after all current operations finish.

Add Operations

Operations are added to an OperationQueue using addOperation(_:) for a single operation or addOperations(_:waitUntilFinished:) for multiple operations. The queue schedules them based on dependencies, priority, and concurrency limits.

Example: Adding Multiple Operations

swift
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2
queue.qualityOfService = .userInitiated

let operation1 = BlockOperation {
    print("Operation 1 on \(Thread.current)")
    Thread.sleep(forTimeInterval: 1)
}
operation1.queuePriority = .high

let operation2 = BlockOperation {
    print("Operation 2 on \(Thread.current)")
    Thread.sleep(forTimeInterval: 0.5)
}
operation2.queuePriority = .normal

queue.addOperations([operation1, operation2], waitUntilFinished: false)
queue.addBarrierBlock {
    print("All operations complete")
}

Output:

Operation 1 on <Thread 0x...>
Operation 2 on <Thread 0x...>
Operation 2 completed
Operation 1 completed
All operations complete

Operations Dependencies

Dependencies ensure operations execute in a specific order by making one operation wait for others to complete. Use addDependency(_:) to establish relationships. Dependencies are resolved before queuePriority or QoS.

Example: Chaining Operations with Dependencies

swift
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2

let downloadOp = BlockOperation {
    print("Downloading data on \(Thread.current)")
    Thread.sleep(forTimeInterval: 2)
}
let processOp = BlockOperation {
    print("Processing data on \(Thread.current)")
    Thread.sleep(forTimeInterval: 1)
}
let saveOp = BlockOperation {
    print("Saving data on \(Thread.current)")
    Thread.sleep(forTimeInterval: 0.5)
}

// Set dependencies
processOp.addDependency(downloadOp)
saveOp.addDependency(processOp)

queue.addOperations([downloadOp, processOp, saveOp], waitUntilFinished: false)
queue.addBarrierBlock {
    print("Workflow complete")
}

Output:

Downloading data on <Thread 0x...>
Processing data on <Thread 0x...>
Saving data on <Thread 0x...>
Workflow complete

Explanation:

  • processOp waits for downloadOp, and saveOp waits for processOp.
  • The queue ensures dependencies are respected, executing operations sequentially despite maxConcurrentOperationCount = 2.

Dispatch Group Implementation Using Operation Queue

A DispatchGroup can be combined with an OperationQueue to synchronize the completion of multiple operations, similar to GCD’s group functionality. This is useful when you need to wait for a set of operations to finish before performing a final task, such as updating the UI.

Example: Using DispatchGroup with OperationQueue

swift
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 3
let group = DispatchGroup()

var results: [Int] = []
let lock = NSLock() // Ensure thread-safe array access

for i in 1...5 {
    group.enter()
    let operation = BlockOperation {
        print("Processing task \(i) on \(Thread.current)")
        Thread.sleep(forTimeInterval: Double.random(in: 0.5...1.5))
        lock.withLock {
            results.append(i * 2)
        }
        group.leave()
    }
    queue.addOperation(operation)
}

group.notify(queue: .main) {
    print("All tasks complete: \(results.sorted())")
}

Output (order varies):

Processing task 1 on <Thread 0x...>
Processing task 2 on <Thread 0x...>
Processing task 3 on <Thread 0x...>
Processing task 4 on <Thread 0x...>
Processing task 5 on <Thread 0x...>
All tasks complete: [2, 4, 6, 8, 10]

Explanation:

  • Each operation enters the DispatchGroup before starting and leaves upon completion.
  • NSLock ensures thread-safe appends to results.
  • group.notify runs on the main queue after all operations finish, printing the results.

Use Case: Fetching data from multiple APIs, processing results, and updating the UI once all tasks are done.

Benefits of Operation Queues Over GCD

While GCD is lightweight and efficient, OperationQueue offers several advantages for complex workflows:

  • Dependency Management: Built-in support for operation dependencies, unlike GCD, which requires manual synchronization (e.g., using DispatchGroup).
  • Cancellation: Operations can be cancelled individually or collectively, with built-in isCancelled checks. GCD tasks require manual cancellation logic.
  • State Management: Operations track lifecycle states (isExecuting, isFinished), simplifying complex task coordination compared to GCD’s stateless blocks.
  • Priority and QoS: Fine-grained control over scheduling with queuePriority and qualityOfService, complementing GCD’s QoS.
  • Reusability: Operations encapsulate tasks as objects, enabling reuse and customization, unlike GCD’s one-off closures.
  • Progress Tracking: Integrates with Progress for user-facing tasks, which GCD lacks natively.
  • Suspension: Supports pausing/resuming the entire queue via isSuspended, not directly available in GCD.
  • Structured Workflows: Ideal for workflows with interdependent tasks, such as download → process → save, compared to GCD’s queue-based model.

When to Use OperationQueue:

  • Complex task chains with dependencies or cancellation requirements.
  • Tasks needing progress reporting or state monitoring.
  • Workflows requiring pause/resume functionality.

When to Use GCD:

  • Simple, independent tasks (e.g., one-off network requests).
  • Low-level concurrency with fine-grained control.
  • High-performance scenarios where overhead must be minimized.

Create an Asynchronous Operation

Asynchronous operations are custom Operation subclasses that perform tasks non-blocking, such as network requests or file I/O. You override start() instead of main(), manage state properties (isExecuting, isFinished), and ensure KVO compliance. Asynchronous operations are critical for tasks that don’t complete immediately.

Example: Asynchronous Image Download Operation

swift
class AsyncImageDownloadOperation: Operation {
    private let url: URL
    private var imageData: Data?
    private var _executing = false
    private var _finished = false
    private var task: URLSessionDataTask?

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

    override var isAsynchronous: Bool { true }

    override var isExecuting: Bool {
        get { _executing }
        set {
            willChangeValue(forKey: "isExecuting")
            _executing = newValue
            didChangeValue(forKey: "isExecuting")
        }
    }

    override var isFinished: Bool {
        get { _finished }
        set {
            willChangeValue(forKey: "isFinished")
            _finished = newValue
            didChangeValue(forKey: "isFinished")
        }
    }

    override func start() {
        guard !isCancelled else {
            isFinished = true
            return
        }
        isExecuting = true
        print("Starting download for \(url) on \(Thread.current)")
        task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
            defer {
                self?.isExecuting = false
                self?.isFinished = true
            }
            guard let self = self, !self.isCancelled else {
                print("Download cancelled for \(self?.url.absoluteString ?? "")")
                return
            }
            if let error = error {
                print("Download failed: \(error)")
                return
            }
            self.imageData = data
            print("Download complete for \(self.url)")
        }
        task?.resume()
    }

    override func cancel() {
        super.cancel()
        task?.cancel()
        isExecuting = false
        isFinished = true
        print("Cancelled download for \(url)")
    }

    func getImageData() -> Data? { imageData }
}

// Usage with OperationQueue
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2

let url = URL(string: "https://example.com/image.jpg")!
let downloadOp = AsyncImageDownloadOperation(url: url)
downloadOp.completionBlock = {
    if let data = downloadOp.getImageData() {
        print("Downloaded image with size: \(data.count) bytes")
    }
}
queue.addOperation(downloadOp)

// Cancel after 1 second
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
    downloadOp.cancel()
}

Explanation:

  • isAsynchronous = true indicates the operation runs asynchronously.
  • start() initiates a URLSession data task and manages state.
  • cancel() stops the network task and updates state.
  • KVO-compliant properties (isExecuting, isFinished) ensure queue compatibility.
  • defer guarantees state cleanup after the task completes or fails.
  • Output (if cancelled early):
    Starting download for https://example.com/image.jpg on <Thread 0x...>
    Cancelled download for https://example.com/image.jpg

Example: Chaining Asynchronous Operations

swift
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2

let downloadOp = AsyncImageDownloadOperation(url: URL(string: "https://example.com/image.jpg")!)
let processOp = BlockOperation {
    guard !downloadOp.isCancelled, let data = downloadOp.getImageData() else {
        print("Processing cancelled or no data")
        return
    }
    print("Processing image with size \(data.count) on \(Thread.current)")
    Thread.sleep(forTimeInterval: 1) // Simulate processing
}
processOp.addDependency(downloadOp)

queue.addOperations([downloadOp, processOp], waitUntilFinished: false)
queue.addBarrierBlock {
    print("Image workflow complete")
}

Explanation:

  • processOp depends on downloadOp, ensuring it only runs after the download completes.
  • The queue manages concurrency and dependencies automatically.
  • addBarrierBlock confirms workflow completion.

Use Case: Asynchronous operations are ideal for network requests, file I/O, or tasks involving external resources where blocking is impractical.

Best Practices

  • Check Cancellation: Always check isCancelled in operations to support graceful termination.
  • Manage Dependencies: Avoid circular dependencies to prevent deadlocks.
  • Limit Concurrency: Set maxConcurrentOperationCount to balance performance and resource usage.
  • Use Completion Blocks: Leverage completionBlock for post-execution tasks.
  • Thread Safety: Use locks or serial queues for shared resources in concurrent operations.
  • Integrate with Progress: Use Progress for user-facing workflows.
  • Combine with GCD: Dispatch UI updates to DispatchQueue.main or use GCD for simpler tasks.
  • Test Cancellation: Ensure operations handle cancellation correctly to avoid resource leaks.

Limitations

  • Overhead: OperationQueue has more overhead than GCD for simple tasks.
  • State Management Complexity: Custom asynchronous operations require careful KVO compliance.
  • Main Thread Safety: UI updates must be dispatched manually to the main queue.
  • Learning Curve: Dependencies and state management can be complex for beginners.

Released under the MIT License.