ACTORs, a way to avoid RACE condition in Swift
What is a race condition in general?
A race condition is an event that occurs when two or more threads/processes access shared data and try to modify it at the same time, which leads to unpredictable behaviour. The output completely depends on the timing & sequence of events in which order happens, it is very difficult to reproduce & debug.
For example, imagine two threads trying to update a variable at the same time. If the variable is initially set to 0, and both threads increment it, the final value should be 2. However, due to a race condition, the final value might end up being 1 instead of 2, because one thread might increment the variable before the other, leading to unexpected behaviour.
Different ways to prevent Race conditions in Swift
Grand Central Dispatch (GCD)
Synchronized blocks
Atomic operations
Dispatch barriers
Semaphores
Actors (My personal favourite).
You can expect details of concepts other than Actors soon (Let me know if required early).
First of all please observe the following code & its output
struct Bank {
// Shared data
private var balance = 1200
let account: String
init(account: String) {
self.account = account
}
mutating func withdraw(value: Int, from: String) {
print("From \(from): checking if balance containing sufficent money")
if balance > value {
print("From \(from): Balance is sufficent, please wait while processing withdrawal")
Thread.sleep(forTimeInterval: Double.random(in: 0...2))
balance -= value
print("From \(from): Done: \(value) has been withdrawed & current balance is \(balance) ")
} else {
print("From \(from): Can't withdraw: insufficent balance")
}
}
}
var account = Bank(account: "My Bank Account is 09089089")
func wifeWithdrawFromATM() {
let queue = DispatchQueue(label: "ATMWithdrawalQueue", attributes: .concurrent)
queue.async {
account.withdraw(value: 600, from: "ATM")
}
}
func husbandWithdrawFromUPI() {
let queue = DispatchQueue(label: "UPIWithdrawalQueue", attributes: .concurrent)
queue.async {
account.withdraw(value: 900, from: "UPI")
}
}
wifeWithdrawFromATM()
husbandWithdrawFromUPI()
Once you run the above code in the playground you might expect output something like that, (it completly depends on timing),
Please note that the initially balance = 1200
From ATM: checking if balance containing sufficent money
From ATM: Balance is sufficent, please wait while processing withdrawal
From UPI: checking if balance containing sufficent money
From UPI: Balance is sufficent, please wait while processing withdrawal
From UPI: Done: 900 has been withdrawed & current balance is 300
From ATM: Done: 600 has been withdrawed & current balance is -300
From the above implementation if can observe the piece of code Thread.sleep(forTimeInterval: Double.random(in: 0...2))
which is performing some computation & processing before deducting the amount from the balance
.
When ATM
thread's request is in process, then at the exact moment of time UPI
thread's request will be in the same block of code, which will lead to the Race condition.
Since the UPI thread had already deducted the amount, the in case of the ATM thread, it must not have been deducted at all & should have been printed From ATM: Can't withdraw: insufficient balance
Let's bring Actors to solve this problem, although others can too solve this problem, I am biased towards actor
as I wanna give oscar award to actor
Question: What is an actor
in Swift?
Answer: An actor
is a type that encapsulates state and behaviour and enforces thread safety by guaranteeing that only one actor can access its mutable state at a time.
Actors are a concurrency primitive that enables safe and efficient concurrent programming.
When multiple tasks access an actor's state, the actor ensures that each access occurs serially and that the state is always in valid state.
BTW the actor
was introduced in Swift 5.5
. with usage of actor,
there come some new keywords too, such as await
& Task
which helpful to call actor
's behaviour.
Let's solve the above problem with actor
actor Bank {
private var balance = 1200
let account: String
init(account: String) {
self.account = account
}
func withdraw(value: Int, from: String) {
print("From \(from): checking if balance containing sufficent money")
if balance > value {
print("From \(from): Balance is sufficent, please wait while processing withdrawal")
Thread.sleep(forTimeInterval: Double.random(in: 0...2))
balance -= value
print("From \(from): Done: \(value) has been withdrawed & current balance is \(balance) ")
} else {
print("From \(from): Can't withdraw: insufficent balance")
}
}
}
var account = Bank(account: "My Bank Account is 09089089")
func wifeWithdrawFromATM() {
Task {
await account.withdraw(value: 600, from: "ATM")
}
}
func husbandWithdrawFromUPI() {
Task {
await account.withdraw(value: 900, from: "UPI")
}
}
wifeWithdrawFromATM()
husbandWithdrawFromUPI()
Output is
From ATM: checking if balance containing sufficent money
From ATM: Balance is sufficent, please wait while processing withdrawal
From ATM: Done: 600 has been withdrawed & current balance is 600
From UPI: checking if balance containing sufficent money
From UPI: Can't withdraw: insufficent balance
As you can observe in the output, all calls to shared data have synchronized, even though this piece of code still exist Thread.sleep(forTimeInterval: Double.random(in: 0...2))
as previous.
Also please observe actor
doesn't use DispatchQueue
API like earlier, here Task
& await
are being used.
Some important things to note above actor
, it is
Reference type like
class
, not a value typestruct & enums
Even being a reference type it does not participate in the inheritance concept of OOPS.
Doesn't uses
mutating
keyword unlike thestruct
is shown above.the
actor
is like a class withfinal
keyword & some internally managed synchronisation mechanisms for shared data.
Below are some of the properties an actor
possesses.
Isolation: An actor encapsulates a mutable state and allows access to that state only through its methods, ensuring that the state cannot be accessed directly from outside the actor.
Concurrency: Actors allow concurrent access to their methods, which can be invoked from multiple threads at the same time, without causing data races.
Synchronous message passing: Messages sent to an actor are processed sequentially and in order, which ensures that the actor's state is accessed safely and predictably.
Asynchronous execution: When a message is sent to an actor, the sender continues to execute immediately without waiting for a response, allowing for efficient use of system resources.
Cancellation: Actors can be cancelled, which means that any in-flight messages are cancelled, and the actor stops processing new messages. This helps to manage system resources and prevent resource leaks.
Hope the actor
s concepts are now clear.
You can reach out to me via Linked In or https://nasirmomin.web.app
Subscribe to my newsletter
Read articles from Nasir directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Nasir
Nasir
App developer in iOS & Flutter