← Back to DevBytes

macOS NSOperation Queues

Introduction to NSOperationQueue

Concurrency is one of the most important concepts in modern software development, and macOS provides several tools to handle it. Among them, NSOperationQueue stands out as a high-level, object-oriented abstraction built on top of Grand Central Dispatch (GCD). It allows developers to manage the execution of operations with fine-grained control over dependencies, priorities, and concurrency.

In this tutorial, we will explore what NSOperationQueue is, why it matters, how to use it effectively in your macOS applications, and the best practices you should follow to get the most out of it.

What is NSOperationQueue?

NSOperationQueue is a class provided by the Foundation framework that regulates the execution of a set of NSOperation objects. An operation represents a single unit of work, and the queue is responsible for scheduling and executing those operations based on their readiness, priority, and dependencies.

Unlike raw GCD, which uses a function-based approach with closures, NSOperationQueue introduces an object-oriented model. This means you can subclass NSOperation, observe its state using Key-Value Observing (KVO), cancel operations, and define complex dependency graphs between tasks.

Under the hood, NSOperationQueue still leverages GCD for thread management, but it adds a layer of features that make it more suitable for complex workflows.

Why NSOperationQueue Matters

While GCD is powerful and lightweight, it lacks some higher-level features that are often needed in real-world applications. NSOperationQueue fills that gap by providing:

These features make NSOperationQueue the preferred choice when your application needs structured, manageable, and observable background work.

Getting Started with NSOperationQueue

Creating a Queue

Creating an operation queue is straightforward. You instantiate an NSOperationQueue and optionally configure its concurrency behavior.

import Foundation

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

In this example, we limit the queue to executing at most two operations at the same time and set the Quality of Service (QoS) to .userInitiated, which tells the system that this work is important for user interaction.

Adding Block Operations

The simplest way to add work to a queue is by using BlockOperation, which wraps one or more closures.

let operation = BlockOperation {
    print("Performing work on thread: \(Thread.current)")
}

queue.addOperation(operation)

You can also add a block directly to the queue, which internally creates a BlockOperation for you:

queue.addOperation {
    print("This is a simple block operation")
}

Using NSBlockOperation with Multiple Blocks

A single BlockOperation can contain multiple blocks, which are executed concurrently:

let multiBlockOperation = BlockOperation()

multiBlockOperation.addExecutionBlock {
    print("Block 1 executing")
}

multiBlockOperation.addExecutionBlock {
    print("Block 2 executing")
}

multiBlockOperation.addExecutionBlock {
    print("Block 3 executing")
}

queue.addOperation(multiBlockOperation)

When this operation runs, all three blocks may execute concurrently, depending on available system resources.

Managing Dependencies

One of the most powerful features of NSOperationQueue is the ability to define dependencies between operations. A dependent operation will not start until all of its dependencies have finished executing.

let downloadOperation = BlockOperation {
    print("Downloading data...")
    Thread.sleep(forTimeInterval: 1)
}

let parseOperation = BlockOperation {
    print("Parsing downloaded data...")
}

let saveOperation = BlockOperation {
    print("Saving parsed data to disk...")
}

parseOperation.addDependency(downloadOperation)
saveOperation.addDependency(parseOperation)

queue.addOperations([downloadOperation, parseOperation, saveOperation], waitUntilFinished: false)

In this example, the operations will always execute in the correct order: download, then parse, then save. The queue handles the scheduling automatically.

You can also remove dependencies at runtime if needed:

parseOperation.removeDependency(downloadOperation)

Creating Custom NSOperation Subclasses

For more complex tasks, you can create a custom subclass of NSOperation. There are two types of custom operations: non-concurrent and concurrent.

Non-Concurrent Operations

A non-concurrent operation performs its work synchronously within the main() method. The queue automatically manages the thread for you.

class ImageProcessingOperation: Operation {
    let imageURL: URL

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

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

        print("Processing image at: \(imageURL.path)")
        // Simulate image processing
        Thread.sleep(forTimeInterval: 2)

        guard !isCancelled else { return }
        print("Finished processing image")
    }
}

let imageOp = ImageProcessingOperation(imageURL: URL(fileURLWithPath: "/tmp/photo.png"))
queue.addOperation(imageOp)

Notice the isCancelled checks. These are important because they allow the operation to exit early if it has been cancelled, which is a best practice for long-running tasks.

Concurrent Operations

Concurrent operations are more complex because they manage their own asynchronous execution. You must override several properties and methods, and you are responsible for generating KVO notifications for the operation's state.

class NetworkFetchOperation: Operation {
    private var _executing = false
    private var _finished = false
    let url: URL

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

    override var isAsynchronous: Bool {
        return true
    }

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

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

    override func start() {
        guard !isCancelled else {
            isFinished = true
            return
        }

        isExecuting = true
        performNetworkRequest()
    }

    private func performNetworkRequest() {
        let task = URLSession.shared.dataTask(with: url) { data, response, error in
            if let error = error {
                print("Network error: \(error.localizedDescription)")
            } else if let data = data {
                print("Received \(data.count) bytes")
            }
            self.isExecuting = false
            self.isFinished = true
        }
        task.resume()
    }
}

This example demonstrates the key requirements of a concurrent operation: overriding isAsynchronous, isExecuting, isFinished, and implementing start() instead of main(). The KVO notifications are critical because the queue relies on them to know when the operation has completed.

Canceling Operations

Operations can be cancelled individually or in bulk through the queue. Cancellation is cooperative, meaning the operation itself must check its isCancelled property and exit gracefully.

let longRunningOp = BlockOperation {
    for i in 0..<100 {
        if Operation.current?.isCancelled == true {
            print("Operation was cancelled at iteration \(i)")
            return
        }
        Thread.sleep(forTimeInterval: 0.1)
    }
}

queue.addOperation(longRunningOp)

Thread.sleep(forTimeInterval: 1)
longRunningOp.cancel()

To cancel all operations in a queue, use:

queue.cancelAllOperations()

This marks all queued and executing operations as cancelled, but it does not guarantee immediate termination. Each operation must handle cancellation on its own.

Waiting for Operations to Complete

Sometimes you need to wait for operations to finish before proceeding. You can wait on a single operation or on the entire queue.

// Wait for a single operation
operation.waitUntilFinished()

// Wait for all operations in the queue
queue.waitUntilAllOperationsAreFinished()

Be cautious when using these methods on the main thread, as they will block the UI. They are best used in background contexts or during testing.

Operation Priorities

Each NSOperation has a queuePriority property that influences the order in which operations are started when multiple operations are ready to run.

let lowPriorityOp = BlockOperation { print("Low priority") }
let highPriorityOp = BlockOperation { print("High priority") }

lowPriorityOp.queuePriority = .low
highPriorityOp.queuePriority = .high

queue.addOperation(lowPriorityOp)
queue.addOperation(highPriorityOp)

Note that priority only affects operations that are ready to execute. If an operation has unmet dependencies, its priority is irrelevant until those dependencies are resolved.

Quality of Service

In addition to operation-level priorities, you can set the Quality of Service at both the queue and operation level. QoS tells the system how much energy and CPU resources to dedicate to the work.

queue.qualityOfService = .utility

let backgroundOp = BlockOperation { print("Background work") }
backgroundOp.qualityOfService = .background
queue.addOperation(backgroundOp)

The available QoS levels, from highest to lowest, are: .userInteractive, .userInitiated, .utility, and .background. Choose the appropriate level to balance responsiveness with energy efficiency.

Best Practices

Conclusion

NSOperationQueue is a robust and flexible concurrency tool that builds on the power of Grand Central Dispatch while adding object-oriented features like dependencies, cancellation, KVO-compliant state, and concurrency limits. By understanding how to create operations, manage dependencies, implement custom subclasses, and follow best practices, you can build macOS applications that handle complex background work in a clean, maintainable, and efficient way. Whether you are processing images, fetching network data, or orchestrating multi-step workflows, NSOperationQueue provides the structure and control needed to get the job done right.

— Ad —

Google AdSense will appear here after approval

← Back to all articles