Concurrency
Introduction to Operation and OperationQueues in iOS and Swift
For this post I assume you are already familiar with Grand Central Dispatch and the basics of concurrency in iOS.
Let’s say you’re running a salad restaurant. You’re at the till. A customer arrives and places an order. You pass on the salad making responsibility to the team in the kitchen. To make the salad:
- Add lettuce
- Add tomatoes
- Add red onion
- Add sweetcorn
- Add tuna with sunflower oil
The process of making the salad has to go in this order. It’s one of those fancy salad that are laid out in a specific way. Except for the first step every subsequent step relies on the completion of the previous step.
How does a salad maker looks like in code? Let’s look at an example:
let saladBowl = SaladBowl()LettucePrepper().prep(saladBowl, onCompletion: { saladBowl in
TomatoesPrepper().prep(saladBowl, onCompletion: { saladBowl in
RedOnionPrepper().prep(saladBowl, onCompletion: { saladBowl in
SweetcornPrepper().prep(saladBowl, onCompletion: { saladBowl in
TunaPrepper().prep(saladBowl, onCompletion: { saladBowl in
// deliver salad
})
})
})
})
})
Each instance (lettucePrepper
, tomatoesPrepper
, redOnionPrepper
, sweetcornPrepper
and tunaPrepper
) represents one task. Once an order is placed anyone can pick up the first task. As soon as they complete the task the onComplete
function is called with the results to delivered ready to passed on the next task to perform their bit.
Notice how tomatoesPrepper
depends on lettucePrepper
to complete their task before tomatoesPrepper
can start. redOnionPrepper
depends on tomatoesPrepper
and so on. Also notice how the code gets nested to follow the order. What if you had 20 steps instead of 5? You’ll have to nest 20 times. This code can soon get ugly.
So is there a better way to execute code without nesting? Is there a better way to visualise the dependencies between each step?
Yes of course there is! The answer is in the title of this post 😉. In this post we’ll be exploring managing dependent operations using Operation and OperationQueues.
Managing dependencies with Operation and OperationQueues
In this section we’ll solve our salad making problem with Operation and OperationQueues through an example app. We’ll start with an existing app. Then we’ll create a class to represent our salad ingredient prepping as an Operation. Finally we’ll convert the salad maker code to use Operation and OperationQueues.
Here is a summary of the steps we are going to take:
- Download the starter project
- Create an ingredient prepping Operation
- Make SaladMaker use the ingredient prepping Operation and OperationQueues
1. Download the starter project
Let’s start by downloading the starter pack. Open Terminal app and execute the following commands:
cd $HOME
curl https://github.com/anuragajwani/intro-operation-and-queues/archive/refs/tags/starter.zip -o starter-operation-queues.zip -L -s
unzip -q starter-operation-queues.zip
cd intro-operation-and-queues-starter
open -a Xcode SaladMaker.xcodeproj
In this post we’ll mostly focusing on the SaladMaker
class in SaladMaker.swift
file.
2. Create an ingredient prepping Operation
In the intro example each Prepper
performs one single task i.e. prepping lettuce. Operations encapsulate a single task to be performed. Let’s represent Prepping and ingredient as an Operation. Add the following code at the end of SwiftMaker.swift
:
class IngredientPrepper: Operation { private let saladBowl: SaladBowl
private let ingredient: Ingredient init(saladBowl: SaladBowl, ingredient: Ingredient) {
self.saladBowl = saladBowl
self.ingredient = ingredient
} override func main() {
sleep(1)
self.saladBowl.ingredients.append(self.ingredient)
}
}
In the code above we have created a single Prepper class which subclasses the Operation
class. The Operation class is meant to be subclassed and not used directly. That is we can’t create an instance of the Operation
class.
Because the
Operation
class is an abstract class, you do not use it directly but instead subclass
In the code we only need to override one function to make our operation work. The function to override is the main
function.
For non-concurrent operations, you typically override only one method: main
Here we include our code to execute. Note we aren’t executing our code asynchronously in a separate thread like we were doing before. Don’t worry about this as we’ll cover how we will do that in the next step.
You can delete the other Prepper
s classes as we will no longer be using these.
3. Make SaladMaker use the ingredient prepping Operation and OperationQueues
In the previous section we created an Ingredient prepping operation. Next let’s change our messy SaladMaker
make
function implementation from using callbacks to using the IngredientPrepper
operation and OperationQueues.
Replace the make function code to the following:
let saladBowl = SaladBowl()
let ingredients: [Ingredient] = [.lettuce, .tomatoes, .redOnion, .sweetcorn, .tuna] //1 List ingredients in order
var operations: [Operation] = []
for (index, ingredient) in ingredients.enumerated() {
let operation = IngredientPrepper(saladBowl: saladBowl, ingredient: ingredient) // 2 create operation for each ingredient
operation.completionBlock = {
onIngrdientPrepped(ingredient)
}
if index > 0 { // 3 add previous ingredient operation as dependency (except for the first one (lettuce)
let previousOperation = operations[index - 1]
operation.addDependency(previousOperation)
}
operations.append(operation)
}
let operationQueue = OperationQueue() // 4 create operation queue
operationQueue.maxConcurrentOperationCount = 1 // 7
operationQueue.addOperations(operations, waitUntilFinished: false) // 5
operationQueue.addBarrierBlock {
// 6 handling completion of all operations
completionHandler()
}
In the code above we start off by setting the list of ingredients to include in the salad. Then we loop through the list of ingredients in order and create an instance of IngredientPrepper
operation for each ingredient. Next we make the previous ingredient preparation as a dependency unless its the first ingredient (lettuce in this case).
At this point all operations have been created and their dependent operation (previous ingredient prepping operation) stated. Next we create an OperationQueue instance and add all the operations to the queue. Finally we completion handler (addBarrierBlock
) to handle when all operations are completed.
You might be asking where do we state the thread or queue in which our operations are executing. OperationQueues by default execute all operations in a separate thread.
Operation queues use the Dispatch framework to initiate the execution of their operations. As a result, queues always invoke operations on a separate thread
OperationQueue’s infact make use of DispatchQueues under the hood. Note in the comment number 7 we are setting to maxConcurrentOperationCount
to 1. OperationQueue will manage the number of dispatch queue’s for you based on this configuration.
That’s it! Run the app and see it in action.
Summary
In this post we learnt:
- how to create an operation
- how to declare dependencies between operations
- how to create operations queue
- how to handle the completion of all operations within a queue
Final Thoughts
You can find the full source code in my Github repositories:
Operations and operations queue add easy extensibility to your asynchronous code. It makes it easier to read and thus manage. However there is a new solution included within Swift 5.5 that can make it even simpler and easier–that is async and await language features. I will be covering that in a post in the future.