Leave a rating/review
Simple example
Build and run the DataRace project in the starter folder. The screen just displays the default message Hello, world! while the two for-loops in raceYou() run until these messages appear in the console:
>>> Main Queue counter = 19
>>> notMain Queue counter = 20
Or the other way around, maybe with a different counter value on the first line. There are no build or runtime errors but, in ContentView, look at the raceYou() code:
var counter = 0
let queue = DispatchQueue(label: "notMain")
queue.async {
for _ in 1 ... 10 {
Thread.sleep(forTimeInterval: Double.random(in: 0.1..<0.5))
counter += 1
}
print(">>> notMain Queue counter = \(counter)")
}
DispatchQueue.main.async {
for _ in 1 ... 10 {
Thread.sleep(forTimeInterval: Double.random(in: 0.1..<0.5))
counter += 1
}
print(">>> Main Queue counter = \(counter)")
}
Both queues are changing counter at the same time! With random sleep times, so sometimes the main queue finishes first, and other times the notMain queue finishes first. The problem is easy to see here but, in a large app, it could be hidden in different files. Here’s where TSan Thread Sanitizer can help. First, enable TSan: Select Edit Scheme. In the Run target, select Diagnostics. Check the Thread Sanitizer box. Build and run again.
WARNING: ThreadSanitizer: Swift access race (pid=13189)
...
SUMMARY: ThreadSanitizer: Swift access race ... in ContentView.raceYou()
==================
ThreadSanitizer report breakpoint hit.
This time, you get a warning and a Thread-Sanitizer report breakpoint. There’s also a purple number next to the status field. Click it to see the Runtime issues.
An important note: TSan performs runtime analysis: If a race condition never happens at runtime, TSan won’t help. It can’t detect something that doesn’t happen. To see this, comment out one of the sleep statements. Build and run again. Now it works fine, with no warnings from TSan.
Close this project, then open Threadsafe.playground in the starter folder.
Thread-safe Class Playground
An object type is thread-safe if concurrent tasks can access its instances without causing any concurrency problems. If a variable or mutable data structure is not thread-safe, you should access it from only one thread at a time.
Swift arrays and dictionaries are not thread-safe.
In this playground, multiple threads change instances of a class that is not thread-safe. You’ll see what goes wrong, then you’ll rewrite the class so only one thread at a time can change an instance. This fixes the problem by making the class thread-safe.
Look at Person.swift: You initialize Person objects with firstName and lastName. The changeName method has the same arguments. Random delays simulate being part of a bigger app.
Back to the playground: You initialize this nameChangingPerson instance to Alison Anderson. Then you ask nameChangingPerson to change name to Brian Biggles. And ask the playground to show the name.
Click right next to line 13 to run the playground up to this point:
"Brian Biggles" (in sidebar)
And it worked: the name changed to Brian Biggles.
But what happens if you call changeName from a concurrent queue? Here’s a custom concurrent workerQueue and a dispatch group, to do something when all the tasks finish. This array of names – Charlie Cheesecake, Delia Dingle and so on — makes it easy to see when the changeName task goes wrong.
Now look at this for-loop:
for (idx, name) in nameList.enumerated() {
workerQueue.async(group: nameChangeGroup) {
usleep(UInt32(10_000 * idx))
nameChangingPerson.changeName(firstName: name.0, lastName: name.1)
print("Current Name: \(nameChangingPerson.name)")
}
}
This loops through nameList, dispatching asynchronously into the nameChangeGroup, on the concurrent workerQueue. Each task sleeps very briefly — a few hundredths of a second — before calling changeName with the firstName and lastName of the current loop item. Then it prints the name.
You expect this loop will print Charlie Cheesecake, Delia Dingle and so on, not necessarily in the same order as nameList.
Next, here’s some dispatch group code:
nameChangeGroup.notify(queue: DispatchQueue.global()) {
print("Final name: \(nameChangingPerson.name)")
PlaygroundPage.current.finishExecution()
}
nameChangeGroup.wait()
After all the tasks finish, you print the final name: the result of the last task to finish executing.
Click next to line 34 to run this, and open the debug area to see what happens:
Current Name: Freddie Dingle
Current Name: Freddie Evershed
Current Name: Freddie Gregory
Current Name: Freddie Frost
Current Name: Freddie Cheesecake
Final name: Freddie Cheesecake
The actual names you see might be different, but the Current and Final Names are inconsistent: Most of them don’t match anyone in nameList. This happens because of data races for both parts of the name — workerQueue is concurrent, and the random delays in changeName let the tasks interfere with each other. The Person class isn’t thread-safe: If you let multiple threads modify it, you’ll get unpredictable results.
Before you fix this, comment out the for-loop and dispatch group code. Now, to make Person thread-safe, you’ll define this ThreadSafePerson as a subclass of Person.
class ThreadSafePerson: Person {
}
First, create a custom concurrent dispatch queue:
let isolationQueue = DispatchQueue(
label: "com.kodeco.person.isolation",
attributes: .concurrent)
Now override changeName to make it a dispatch barrier task on isolationQueue.
You just specify the .barrier flag when you dispatch onto the queue. And the barrier task is just super.changeName.
override func changeName(firstName: String, lastName: String) {
isolationQueue.async(flags: .barrier) {
// barrier task
super.changeName(firstName: firstName, lastName: lastName)
}
}
isolationQueue must be a custom concurrent queue to implement a dispatch barrier. You don’t want to block a global dispatch queue. And you want to run non-barrier tasks concurrently.
That’s right, you’ll also use isolationQueue to control read access to the class properties by overriding the name property:
override var name: String {
isolationQueue.sync {
super.name
}
}
Getting super.name is a synchronous task on isolationQueue. It doesn’t return until it has a value. Because changeName is a barrier task, the value will always be a valid name.
Now, to test your new ThreadSafePerson class, there’s some familiar-looking code below this print statement:
print("\n=== Threadsafe ===")
A new dispatch group:
let threadSafeNameGroup = DispatchGroup()
A ThreadSafePerson object with the name Anna Adams:
let threadSafePerson = ThreadSafePerson(firstName: "Anna", lastName: "Adams")
The dispatch group code is the same, except it uses threadSafeNameGroup and threadSafePerson:
for (idx, name) in nameList.enumerated() {
workerQueue.async(group: threadSafeNameGroup) {
usleep(UInt32(10_000 * idx))
threadSafePerson.changeName(firstName: name.0, lastName: name.1)
print("Current threadsafe name: \(threadSafePerson.name)")
}
}
threadSafeNameGroup.notify(queue: DispatchQueue.global()) {
print("Final threadsafe name: \(threadSafePerson.name)")
sleep(1)
PlaygroundPage.current.finishExecution()
}
Now run the Playground and watch the debug area:
=== Threadsafe ===
Current threadsafe name: Eva Evershed
Current threadsafe name: Eva Evershed
Current threadsafe name: Eva Evershed
Current threadsafe name: Freddie Frost
Current threadsafe name: Gina Gregory
Final threadsafe name: Gina Gregory
It certainly looks fixed! Current thread safe names are always valid names.
When a changeName task enters isolationQueue, it prevents new tasks from starting and waits for current tasks to finish. When the current tasks have finished, changeName runs all on its own, with exclusive read-write access to the object. The concurrent queue becomes temporarily serial. No other changeName task can sneak in and change firstName or lastName while this changeName task is setting these values. So the name it creates matches its input arguments exactly.
The names are always in order, but you can get duplicate names when the next changeName sneaks in before the previous task’s print statement. Remember, the barrier task is only the changeName method. The print statement waits its turn with the rest of the non-barrier tasks. And remember, it’s waiting synchronously in isolationQueue. So by the time it gets a name, sometimes it’s already the next name.
Thread-safe Class Project
Now look at an app version of this code, in NameChanger: It has only a placeholder UI, but you need an app to use TSan. As you did with the DataRace project, edit the scheme to enable TSan, then build and run.
Of course, TSan finds problems — purple flags and warnings in the console. Click on a purple flag to open the issue navigator. All the issues are data race in changeName. And the same down here in the debug console, with the mismatched names in between and at the end.
To fix this, just comment out the bad call, and uncomment the good one.
// TSan finds race condition errors
// changeNameRace()
// TSan finds no errors
changeNameSafely()
Then build and run again.
Current threadsafe name: Charlie Cheesecake
Current threadsafe name: Delia Dingle
Current threadsafe name: Eva Evershed
Current threadsafe name: Freddie Frost
Current threadsafe name: Gina Gregory
Final threadsafe name: Gina Gregory
No problems this time! Remember to turn off TSan: It does add time and stores more info than a normal build.
So that’s how to use dispatch barriers to make a class thread safe. BTW this thread-safe class works with Operations, too. Next, you’ll apply your new knowledge to make a Number class thread-safe.