Hands-On Design Patterns with Swift
上QQ阅读APP看书,第一时间看更新

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")
}
Take a minute to think about what order you expect the logs to be printed in.

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)
Take a minute to write on a piece of paper what you can suppose about the concurrent queue execution order.

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.