Tasks and queues
Queues are objects that abstract series of operations to execute. The system creates many queues for you, but the most important one is probably the main queue. Queues come in two major flavors, as follows:
- Serial: A serial queue guarantees that no two operations will be executed at the same time. This is a particularly interesting feature if you need to write thread-safe code.
- Concurrent: A concurrent queue will let different operations run in parallel, which can also be a very interesting feature.
As we mentioned previously, a serial queue is a queue that guarantees that no two operations will be run in parallel. The main queue of our apps, which you can access through DispatchQueue.main, is a serial queue. This is helpful to know, as the UI of iOS and macOS apps is run on the main thread. The execution of tasks on the main thread will be in order.
Let's consider the following code:
DispatchQueue.main.async {
print("operation 1")
DispatchQueue.main.async {
print("operation 1.1")
}
}
DispatchQueue.main.async {
print("operation 2")
}
The output is as follows:
operation 1
operation 2
operation 1.1
The operations are executed in the order that they were enqueued. This is why operation 2 runs before operation 1.1. This is guaranteed, because the main queue is a serial queue. In a concurrent queue, this would not be guaranteed, and operations could be run out of order.
Let's spice things up and run this series multiple times, with different queues, considering the following function run, which takes a queue and a number of executions. This function will run as many times as it is asked, on the provided queue, using the same logic that we ran previously:
func run(queue: DispatchQueue, times: Int) {
(0..<times).forEach { i in
queue.async {
print("\(i) operation 1")
queue.async { print("\(i) operation 1.1") }
}
queue.async {
print("\(i) operation 2")
}
}
}
When calling with run(queue: .main, times: 3), the results are consistent with a serial queue:
0 operation 1
0 operation 2
1 operation 1
1 operation 2
2 operation 1
2 operation 2
0 operation 1.1
1 operation 1.1
2 operation 1.1
All operations are executed in the order in which they have been enqueued. However, can you guess what will happen if we plug in a concurrent queue? See the following:
let queue = DispatchQueue(label: "com.run.concurrent",
attributes: .concurrent)
run(queue: queue, times: 3)
The execution is actually unpredictable. Running the program multiple times will always yield a different output. Let's take a look at the following table:
As we can clearly see in the preceding table, every run has a slightly different execution order. We've repeated these simple tasks just a few times, and we didn't execute large computations or slow operations that would amplify this behavior.
Let's take a look at how we can use the Dispatch library to coordinate multiple operations with precision, and ensure the order of execution.