Leave a rating/review
Notes: 03. Use Dispatch Queues
You’re going to take a closer look at serial vs concurrent queues, and synchronous vs asynchronous dispatch.
Open the playground in the starter folder. First, I’ll tell you some things about working with playgrounds. Sometimes, the playground opens with a blank page. If that happens, just open the project navigator and click the playground page. Here you also see the playground can have Sources and Resources. When the navigator is closed, you can view Sources files in the navigation menu:
Duration.swift defines a helper function you’ll use in these playgrounds to see how long a task takes on the thread where you call it.
To return to the playground page, double-click it in the navigation menu.
Most of the exercises for this course use playgrounds, which run until you tell them to stop by clicking the stop button.
Importing PlaygroundSupport lets you call this method in completion handlers or other closures to stop the playground.
PlaygroundPage.current.finishExecution()
The sleep command, here and elsewhere in these playgrounds, allows time for a task to finish before executing the next statement. The argument 3 sets the number of seconds to sleep.
OK, back to the top and let’s get started!
Create dispatch queues
Every task eventually ends up being executed on a global dispatch queue. You can dispatch a task directly to a global dispatch queue. In an app, you might dispatch to the userInitiated global queue. You reference this with the DispatchQueue class function global:
let userQueue = DispatchQueue.global(qos: .userInitiated)
You specify the qos quality of service argument. You don’t even need the qos argument to reference the default global queue:
let defaultQueue = DispatchQueue.global()
And the main queue is a DispatchQueue class property:
let mainQueue = DispatchQueue.main
In an app, always dispatch back to the main queue for user interface updates. Here in a playground, you can display results in the sidebar just by writing the object’s name on a line, so there’s much less need to dispatch to the main queue.
Dispatch async to global queue
Here are some simple tasks: Task 1 has a 1-second sleep, to make it take longer to run than task 2.
func task1() {
print("Task 1 started")
// make task1 take longer than task2
sleep(1)
print("Task 1 finished")
}
func task2() {
print("Task 2 started")
print("Task 2 finished")
}
In an app, you shouldn’t call sleep on the main queue: Your app’s user interface will stop. But it’s OK to call sleep in a playground. Now, dispatch these tasks asynchronously onto userqueue:
userQueue.async {
task1()
}
userQueue.async {
task2()
}
You call each task in a closure to the queue’s async method.
Synchronous vs asynchronous tells you whether the current thread is blocked — whether it has to wait for the task to complete. To demonstrate that the current thread is not blocked when you dispatch asynchronously, wrap the dispatch statements in the duration utility you saw in the Sources folder:
duration {
...
}
Now, down at the bottom of the window, hit the play button that says Execute Playground. Look in the debug area to see what gets printed.
=== Starting userInitated global queue ===
Task 1 started
Task 2 started
Task 2 finished
Task 1 finished
Task 1 started and then task 2 started.
Global queues are concurrent: The tasks run at the same time on userQueue but, because of that sleep time in task 1, task 2 finished before task 1. Also look at the duration value in the sidebar. If you can’t see enough of the sidebar, just grab this slidy thing and drag it left. You can also click this button to Show Result.
7.402896881103516e-05
I’m using a MacBook with an M2 chip. Your duration is probably different, but it is a lot less than 1 second, and you know task1 takes at least 1 second. Dispatching the tasks took much less time on this thread, because running the tasks on another queue did not block this thread. This thread did not wait for those tasks to finish.
Create private serial queue
Concurrent execution is the default global queue behavior. What if you want to run tasks serially? One at a time. The only global serial queue is DispatchQueue.main, which you should only use for user interface activity. You can create a private serial queue, if you want tasks to run exactly in the order they arrive. This is useful for ensuring serial access to a resource, to avoid data races or deadlocks. You create a private queue with the DispatchQueue initializer. Serial is the default attribute for a private dispatch queue, so you only need to specify the queue’s label:
let mySerialQueue = DispatchQueue(label: "com.kodeco.serial")
Your label can be any unique value, but something meaningful, like this reverse-DNS-style name, will be useful when you’re debugging.
Dispatch async to private serial queue
Now dispatch the tasks onto your private serial queue:
duration {
mySerialQueue.async {
task1()
}
mySerialQueue.async {
task2()
}
}
Run the playground and look at the output:
=== Starting mySerialQueue ===
Task 1 started
Task 1 finished
Task 2 started
Task 2 finished
In the serial queue, task 1 starts and finishes before task 2 starts and finishes. Task 2 has to wait for that 1-second sleep in task 1.
Create private concurrent queue; dispatch async
Next, you’ll look at a private concurrent queue, something you could use to group the tasks triggered by a user action, keeping them separate from the global queues. In Part 2, you’ll need a private concurrent queue to create a dispatch barrier — it’s one solution for the readers and writers data race problem. To create a private concurrent queue, you specify the .concurrent attribute:
let workerQueue = DispatchQueue(label: "com.kodeco.worker", attributes: .concurrent)
Now do a copy-paste-edit to set up workerQueue tasks the same as before:
duration {
workerQueue.async {
task1()
}
workerQueue.async {
task2()
}
}
Before you run the playground again, uncomment the print statement and also this 2-second sleep after the serial queue tasks, to give them time to finish before the workerQueue tasks start:
sleep(2)
Run the playground and look at the output:
=== Starting workerQueue ===
Task 1 started
Task 2 started
Task 2 finished
Task 1 finished
As you’d expect, the tasks start and finish in the same order as the userInitiated dispatch queue.
Dispatch sync to private serial queue
So far, you’ve always dispatched a task asynchronously, whether the queue is concurrent or serial — the async method returns right away to the current thread, so it can immediately execute the next statement. The concurrent queues create multiple threads to do their work; the serial queue runs tasks on its single thread.
Just as an experiment, go back up to the userQueue and dispatch task 1 synchronously:
duration {
userQueue.sync { // just change this
task1()
}
userQueue.async {
task2()
}
}
Run this block of code — hover over the line number to show the run button — and check the output:
=== Starting userInitated global queue ===
Task 1 started
Task 1 finished
Task 2 started
Task 2 finished
Task 2 doesn’t start until task 1 finishes. Dispatching synchronously to the userQueue blocked the current thread until task 1 finished, so it couldn’t dispatch task 2 until task 1 finished. And the duration is just over 1 second.
1.002355098724365
Click the stop button and change sync back to async.
userQueue.async { task1() }
You have to be very careful calling a queue’s sync method because the current thread has to wait until the task finishes running on the queue. You must also be careful not to create a cycle: If the task you’re dispatching needs the current queue to do something, then your app will deadlock. Never call sync on the main queue because that will almost certainly deadlock your app! OK, you just called sync on the main queue, but you can get away with doing this in a playground.
Data races
sync is very useful for avoiding data races — if the queue is a serial queue, and it’s the only way to access an object, the sync method behaves as a mutual exclusion lock, guaranteeing that all threads get consistent values.
You can create a simple data race by changing value asynchronously on a private queue, while displaying value on the current thread. Here are the value and changeValue function, with a short sleep:
var value = 42
func changeValue() {
sleep(1)
value = 0
}
Now, dispatch changeValue asynchronously onto your private serial queue, and display value on the current thread:
mySerialQueue.async {
changeValue()
}
value
Run the playground and check the sidebar:
value [42 in sidebar]
The current thread displays 42, because it executes before changeValue() finishes. Now reset value, then dispatch changeValue() synchronously.
value = 42
mySerialQueue.sync {
changeValue()
}
value
This blocks the current thread until the changeValue task has finished, thus removing the data race. Before you run this, I’ll tell you about the dispatchPrecondition function, which you can use to stop execution if you’re on the wrong queue.
Add this line just before resetting value:
dispatchPrecondition(condition: .notOnQueue(mainQueue))
This statement will stop execution. Try it: run the playground. You get this error because you actually are on the main queue. So change the condition to its opposite:
dispatchPrecondition(condition: .onQueue(mainQueue))
Now the condition is, that you are on the main queue. Select Product > Clear All Issues, then run the playground again:
value [0 in sidebar]
Now the value on the main queue is 0, because it had to wait for changeValue() to finish.
An interesting implementation detail is that a task dispatched synchronously actually runs on the current thread, regardless of which queue you dispatch it to. It makes sense: Because the current queue has to wait for the task to return, its thread won’t be doing anything, so it might as well do the task! This includes the main thread, if you make the mistake of dispatching synchronously from the main queue. There’s a link below to an article about this totally surprising fact.