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
andDispatchQueue
- 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 Operation
s. 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.
- Create an instance of an
OperationQueue
- Create a
BlockOperation
instance and provide a closure to execute - Set the tasks priority to high
- Add the
Operation
to theOperationQueue
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!
isReady
isfalse
as we haven't created the operation yet, just an optionalBlockOperation
with no value.isReady
is nowtrue
as the operation has been created with some work to perform. It's 'ready' for execution.isExecuting
istrue
as we access it inside the block that's running.isExecuting
isfalse
as it's placed inside the completion block of the operation which is called after the work has finished.isFinished
istrue
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 ✌️