How does Swift Actor prevent data race?

Swift Modern concurrency actor, async-await

Ashok Rawat
5 min readOct 20, 2022

What is Data Race?

A data race occurs when two or more threads in a single process access the same memory location concurrently, and at least one of the access is for writing, and the threads are not using any exclusive locks to control their accesses to that memory.

What is Race Condition?

A race condition occurs when two threads access a shared variable at the same time. The first thread reads the variable, and the second thread reads the same value from the variable. It means a race condition occurs when the timing or order of events affects the correctness of a piece of code.

var account= Account(balance: 500)
DispatchQueue.concurrentPerform(iterations: 2) { _ in
self
.bankAccountVM.withdraw(500)
}
// Balance is 0.0
// Balance is -500.0

In a multi-threaded application, multiple threads trigger different withdrawals for the same account. The order of the same two withdraws is unpredictable and can lead to two different outcomes based on the order of execution which ends up with weirder outcomes of balances. There is no synchronization in place.

Prior to Swift actor solving this using synchronise mechanism which can be DispatchQueue, DispatchSemaphore and Lock.

// using DispatchQueuelet lockQueue = DispatchQueue(label: "my.serial.lock.queue")
DispatchQueue.concurrentPerform(iterations: 2) { _ in
lockQueue.sync {
self.bankAccountVM.withdraw(500)
}
}
// using DispatchSemaphorelet semaphore = DispatchSemaphore(value: 1)
DispatchQueue.concurrentPerform(iterations: 2) { _ in
semaphore.wait()
self.bankAccountVM.withdraw(500)
semaphore.signal()
}
// using lockvar lock = os_unfair_lock_s()
DispatchQueue.concurrentPerform(iterations: 2) { _ in
os_unfair_lock_lock(&lock)
self.bankAccountVM.withdraw(500)
os_unfair_lock_unlock(&lock)
}

Actor

Actor is a concrete nominal type in Swift to tackle data synchronisation. They are reference types and can have methods, and properties, and also can implement protocols, but they don’t support inheritance which makes their initializers much simpler — there is no need for convenience initializers, overriding, the final keyword.

Actors are created using the new actor keyword. They can protect the internal state through data isolation ensuring that only a single thread will have access to the underlying data structure at a given time. All actors implicitly conform to a new Actor protocol; no other concrete type can use this. Actors solve the data race problem by introducing actor isolation. Stored properties cannot be written from outside the actor object at all.

If you are writing code inside an actor’s method, you can read other properties on that actor and call its synchronous methods without using await, but if you’re trying to use that data from outside the actor await is required even for synchronous properties and methods.

By default, each method of an actor becomes isolated, which means you’ll have to be in the context of an actor already or use await to wait for approved access to actor contained data. Actor methods are isolated by default but not explicitly marked as so.

Actor parameters as isolated

Using the isolated keyword for parameters can be used less code for solving specific problems. The above code example introduced a deposit method to alter the balance of another bank account.

We could get rid of this extra method by marking the parameter as isolated and update the other bank account balance.

func transfer(amount: Double, to other: isolated ARBankAccount) async throws {
if amount > balance {
throw ARBankError.insufficientFunds(amount)
}
balance -= amount
other.balance += amount
print(“Account: \(balance), Other Account: \(other.balance)”)
}

Nonisolated keyword in actor

Marking methods or properties as nonisolated can be used to opt-out to the default isolation of actors. Opting out can be helpful in cases of accessing immutable values or when conforming to protocol requirements.

1. Accessing immutable values from the computed property
The account number is immutable, therefore safe to access from a non-isolated environment. The compiler is smart enough to recognize this state, so there’s no need to mark this parameter as nonisolated explicitly.

However, if a computed property accesses an immutable property, it will show compile error as below.

Here we have to explicitly mark it as nonisolated to remove the error.

actor ARBankAccount {
let accountNumber: Int
var balance: Double
nonisolated var details: String {
"Account holder Number: \(accountNumber)"
}
init(accountNumber: Int, balance: Double) {
self.accountNumber = accountNumber
self.balance = balance
}
}

2. Protocol conformance with nonisolated
Adding protocol conformance in which we are sure to access immutable state only. Below is an example, where we could replace the details property with the CustomStringConvertible protocol.

Here we can solve this again by explicitly making use of the nonisolated keyword.

extension ARBankAccount: CustomStringConvertible {     
nonisolated var description: String {
"Account holder number: \(accountNumber)"
}
}

MainActor

MainActor is a global actor that allows us to execute code on the main queue. Using the keyword @MainActor global actor you can use to mark properties and methods that should be accessed only on the main thread. It is a global actor wrapper around the underlying MainActor struct, which is helpful because it has a static run() method that lets us schedule work to be run.

Writing properties from outside an actor is not allowed, with or without await.

You can check out the demo project on GitHub for Swift Actor.

Actors in Swift are a great way to synchronize access to a shared mutable state which prevents data race. Async-await in Swift allows for structured concurrency, which will improve the readability of complex asynchronous code. We can easily avoid closures with self-capturing and improve error handling. Check out more about Async-await in the Swift async-await blog.

Thanks for reading. If you have any comments, questions, or recommendations feel free to post them in the comment section below! 👇 Please share and give claps 👏👏 if you liked this post.

--

--

Ashok Rawat

Mobile Engineer iOS | Swift | Objective-C | Python | React | Unity3D | Flutter