This content originally appeared on Level Up Coding - Medium and was authored by Alex Dremov
Unleash the power of Swift concurrency with Actors! Get all the information you need in this comprehensive article
Mobile development is close to impossible without concurrent code. While executing tasks concurrently generally speeds up your app, it also introduces a lot of challenges to overcome. And one of them is a data race.
Data Races And When They Happen
Try to find a problem in the code below
import Foundation
var counter = 0
let queue = DispatchQueue.global()
for _ in 1...100500 {
queue.async {
counter += 1
}
}
queue.sync(flags: .barrier) {
// Synchronous barrier to wait untill all
// async tasks are finished
print("Final value: \(counter)")
}
This does not output 100500 as desired
Final value: 100490
Let me run the same code one more time.
Final value: 100486
Voilà
As you see, the same code produces different results. In this case, we deal with a data race.
💡 Data races occur when multiple threads access a shared resource without protections, leading to undefined behaviour
In the code above, asynchronous tasks capture counter and modify it simultaneously. This leads to undefined behaviour.
What’s under the hood?
The reasoning behind such behaviour is in assembly operations. Before incrementing the value, it is loaded from RAM into the processor’s register. At the same time, other threads can increment the value and save it back to RAM. But the thread that saved value from memory to register will not know about it and will continue to work with the old value, eventually overwriting the updated value in RAM
Non-Actor Solutions
Before the introduction of actors, several solutions to the problem were used.
Serial Queue
We can create a dedicated queue that will be used during all accesses to the counter. Internally, tasks execute serially, so no data races occur.
import Foundation
var counter = 0
let queue = DispatchQueue.global()
// Serial queue
let counterAccessQueue = DispatchQueue(label: "CounterAccessQueue")
for _ in 1...100500 {
queue.async {
counterAccessQueue.sync { counter += 1 }
}
}
queue.sync(flags: .barrier) {
counterAccessQueue.sync { print("Final value: \(counter)") }
}
Concurrent Queue With Barrier
It’s possible to use sync with barrier parameter to modify value even in concurrent queue. Basically, the barrier waits until all previous tasks are completed, then it executes code synchronously, and after that queue continues to operate as usual.
In the current example, it basically transforms concurrent queue to serial, but still, it’s a different approach.
import Foundation
var counter = 0
let queue = DispatchQueue.global()
for _ in 1...100500 {
queue.sync(flags: .barrier) {
counter += 1
}
}
queue.sync {
print("Final value: \(counter)")
}
Actors Model
The actor model is an architecturally different approach. Consider actors as classes with additional restrictions. Ideologically, code inside actors cannot be executed concurrently, therefore actors can safely modify their state.
In the world of chaos (concurrent) consider actors as a safe space
Also, other instances cannot modify the actor’s state from the outside. Thus, ensuring the safety of accesses.
💥 All in all, actors let you safely share information between concurrent contexts
Using Actors in Swift
Luckily, we do not need to implement the actor model ourselves. Starting from Swift 5.7, actors are available as part of Swift concurrency.
Actors are defined with actor keyword.
actor Counter {
private(set) var counter = 0
func increment() {
counter += 1
}
}
💡 Like classes, actors are reference types
Generally, all access to actors may be suspended and require await keyword.
💥 If you’re unfamiliar with Swift concurrency, check out my quick guide!
Quick Guide to Async Await in Swift | Alex Dremov
Now, according to the defined model, an actor represents an isolated state. Therefore, we cannot directly execute code inside the actor or change its state because some other task can already be changing the actor’s state.
We want to mitigate data races!
let counter = Counter()
let queue = DispatchQueue.global()
// Used only to wait for all tasks to complete
let group = DispatchGroup()
for _ in 1...100500 {
group.enter()
queue.async {
// async calls can be executed only in
// appropriate concurrent environment, so
// we spawn a new task
Task.detached {
await counter.increment()
group.leave()
}
}
}
group.wait()
Task {
print("Final value: \(await counter.counter)")
}
As you see, all calls to methods of Counter and even to its properties are asynchronous and marked with await keyword.
💡 Notice that await is not needed inside the actor's method. That's because the actor's methods are already inside an isolated state
Nonisolated Members
All members of actors are by default isolated. Actors also can have non-isolated members. Access to them is the same as if actor was a regular class. Notice, though, that nonisolated methods cannot directly access isolated members.
😡 Stored non-constant properties cannot be nonisolated
💡 Constant properties ( let ) are nonisolated by default, as they cannot provoke a data race
actor Counter {
let id = UUID()
private(set) var counter: Int = 0
private nonisolated var description: String {
"Counter"
}
func increment() {
counter += 1
}
nonisolated func getDescription() -> String {
return description
}
}
...
print(counter.getDescription()) // no await
print(counter.id) // no await
Difference to Locks
One may ask
How’s it different from taking a lock before executing code inside an actor and releasing a lock on an exit?
The difference is noticeable if actor itself runs asynchronous operations inside it. For example, if it messages another actor.
Take a look
actor Ping {
let pong = Pong()
func run() async {
print("ping!")
await pong.run() // Suspension point
}
}
actor Pong {
func run() async {
try! await Task.sleep(for: .seconds(1)) // sleeping a bit
print("pong!")
}
}
let ping = Ping()
Task {
await ping.run()
}
Task{
await ping.run()
}
This code outputs
ping!
ping!
pong!
pong!
Notice that another actor is also called using await keyword. I marked this place as a suspension point. The current task is suspended while waiting for an asynchronous task, so actor is free for entrance again.
That’s the core difference to a simple mutex or lock, and it is called Actor Reentrancy. Some consider this a problem. However, it is an awesome optimization at expense of complicating code a bit.
💥 Mind about actor reentrancy! It is incorrect to make assumptions about actor’s state after an await call inside an actor
actor Door {
private var _open = false
func open() async {
_open = true
await someTask() // Suspension point
// Mistake! Door could have been closed
// while someTask was executing
print("Door is open")
}
func close() {
_open = false
}
}
Luckily, suspension points all are marked with await keyword, so it is easy to keep track of them
Final Notes
Actors are a great solution to data races. They nicely integrate into Swift concurrency. Keep in mind, though, that actor reentrancy must be taken into account to avoid incorrect state assumptions.
This post was initially published on alexdremov.me
Level Up Coding
Thanks for being a part of our community! Before you go:
- 👏 Clap for the story and follow the author 👉
- 📰 View more content in the Level Up Coding publication
- 🔔 Follow us: Twitter | LinkedIn | Newsletter
🚀👉 Join the Level Up talent collective and find an amazing job
Conquer Data Races with Swift Actors was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.
This content originally appeared on Level Up Coding - Medium and was authored by Alex Dremov

Alex Dremov | Sciencx (2023-02-08T15:16:48+00:00) Conquer Data Races with Swift Actors. Retrieved from https://www.scien.cx/2023/02/08/conquer-data-races-with-swift-actors/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.