This is part two of a two-part series on concurrency. Part one introduced you to concurrency as a concept and how to write concurrent code with DispatchQueue. If you haven't already, i'd suggest you check that out here.

In this article you'll cover the following:

  • The differences between OperationQueue and DispatchQueue
  • When to use OperationQueue
  • Creating a queue and adding operations
  • Cancelling operations in a queue and setting priorities
  • Tracking the status of an operation and when it completes
  • Defining dependencies
  • Subclassing Operation

What is OperationQueue?

I've mentioned OperationQueue a few times now, and if you're here you're likely wondering what it is or how/when it should be used. OperationQueue is a class used to carry out the execution of Operations. You can add one or more operations to a queue and it will carry each one out in order of priority, if multiple operations have the same priority it will execute them in the order in which they are added.

...So what's an Operation?

While the above covers what OperationQueue is, it's all a bit pointless without Operation. Operation is an abstract class, for that reason you don't use it directly but instead use one of the system defined subclasses like BlockOperation or NSInvocationOperation.

NSInvocationOperation is an objective-c only class so we won't be covering it in this article, but if you're interested you can read more about it here.

Take a quick look at the following code to create an Operation and schedule it's execution on an OperationQueue.

// 1
let queue = OperationQueue()

// 2
let task = BlockOperation {
    print("Executed an operation")
}

// 3
task.queuePriority = .high

// 4
queue.addOperation(task)

Add the above code to a playground and you'll see  "Executed an operation" printed in the console.

  1. Create an instance of an OperationQueue
  2. Create a BlockOperation instance and provide a closure to execute
  3. Set the tasks priority to high
  4. Add the Operation to the OperationQueue

Operation priorities can be one of the following values.

  • veryLow
  • low
  • normal
  • high
  • veryHigh

Operations in a queue are executed based on their queuePriority. If the queuePriority is the same for multiple operations then they will be executed in the order in which they are added.

It looks a lot like the DispatchQueue, so what's the difference? You'll cover that in the next section.

DispatchQueue vs OperationQueue

If you read part one of this article you're likely asking yourself what the differences are between these two APIs. At face value they seem to do the same thing - and they can be used interchangeably in a lot of cases - but OperationQueue allows for more fine grain control.

DispatchQueue is a lightweight and simple way to represent work. Any task executed on a DispatchQueue is scheduled by the system automatically, you can suspend and resume an operation but doing so isn't overly intuitive.

On the other hand, OperationQueue offers a lot more customisation. You can start an Operation manually then pause, resume and cancel it. You can define dependencies - that is - an operation will not start until it's dependent operations have finished. You can access information about the execution state and be notified of an operation's completion.

Dependencies

The following code demonstrates how dependencies work.

let queue = OperationQueue()
let task = BlockOperation {
    print("Executed an operation")
}

let task2 = BlockOperation {
    print("Executed a second operation")
}

let task3 = BlockOperation {
    print("Executed a third operation")
}

task.addDependency(task2)
task2.addDependency(task3)

queue.addOperation(task)
queue.addOperation(task2)
queue.addOperation(task3)

// "Executed a third operation"
// "Executed a second operation"
// "Executed an operation"

In the above example, even though tasks are added to the queue in the order task, task2, task3, they finished in the the order task3, task2, task. That's because task was waiting for task2 which was waiting for task3.

Completion

Completion blocks work as expected, that is we can define a block of code to run when the code inside the operation is finished.

let queue = OperationQueue()
let task = BlockOperation {
    print("Counting to 100")
    var count = 0
    while count < 100 {
      count += 1
    }
}
task.completionBlock = {
    print("We counted to 100!")
}

queue.addOperation(task)

// Counting to 100
// We counted to 100!

The above simply increments a count to 100 and calls the print statement inside the completion block when it's done.

Accessing State

We can access the state of an operation with the isReady, isRunning and isFinished variables. Below is an expanded example of the above code to demonstrate this.

let queue = OperationQueue()
var task: BlockOperation?

print("1. Is Ready: ", task?.isReady ?? false)
task = BlockOperation {
    print("3. Is executing: ", task!.isExecuting)
    var count = 0
    while count < 100 {
      count += 1
    }
}

print("2. Is Ready: ", task!.isReady)

task!.completionBlock = {
    print("4. Is executing: ", task!.isExecuting)
    print("5. Is Finished: ", task!.isFinished)
    print("We counted to 100!")
}

queue.addOperation(task!)

// 1. Is Ready:  false
// 2. Is Ready:  true
// 3. Is executing:  true
// 4. Is executing:  false
// 5. Is Finished:  true
// We counted to 100!
  1. isReady is false as we haven't created the operation yet, just an optional BlockOperation with no value.
  2. isReady is now true as the operation has been created with some work to perform. It's 'ready' for execution.
  3. isExecuting is true as we access it inside the block that's running.
  4. isExecuting is false as it's placed inside the completion block of the operation which is called after the work has finished.
  5. isFinished is true as it's placed inside the completion block of the operation which is called after the work has finished.

We have one more variable isCancelled. This is true if the operation has been cancelled - that is - cancel() has been called.


Subclassing

BlockOperation is a great system defined subclass of Operation, but the story doesn't end here; operation can be subclassed to create logical units of work.

let queue = OperationQueue()

class CustomDownloadOperation: Operation {

    let downloadTask: URLSessionDownloadTask

    init(downloadTask: URLSessionDownloadTask) {
        self.downloadTask = downloadTask
    }

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

        // Do any extra stuff here
    }
}

let urlString = "https://upload.wikimedia.org/wikipedia/commons/9/9d/Swift_logo.svg"

let request = URLRequest(url: URL(string: urlString)!)
let downloadTask = URLSession.shared.downloadTask(with: request)
let customOperation = CustomDownloadOperation(downloadTask: downloadTask)

customOperation.completionBlock = {
    print("Finished")
}
queue.addOperation(customOperation)

This operation subclass encapsulates all that Operation has to offer and gives us the flexibility to add more custom functionality. This subclass could be expanded to report on progress, pause and resume downloads and provide dependencies.


That was a fairly quick introduction to Operation and OperationQueue. The most important thing to understand here is when to use OperationQueue and when to use DispatchQueue. Apple suggest using the highest level of abstraction and dropping to a lower level only when necessary.

If you haven't checked out part one yet you can do so here.

Thanks for reading ✌️