ACTORs, a way to avoid RACE condition in Swift

·

5 min read

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

  1. Grand Central Dispatch (GCD)

  2. Synchronized blocks

  3. Atomic operations

  4. Dispatch barriers

  5. Semaphores

  6. 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 type struct & enums

  • Even being a reference type it does not participate in the inheritance concept of OOPS.

  • Doesn't uses mutating keyword unlike the struct is shown above.

  • the actor is like a class with final keyword & some internally managed synchronisation mechanisms for shared data.


Below are some of the properties an actor possesses.

  1. 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.

  2. Concurrency: Actors allow concurrent access to their methods, which can be invoked from multiple threads at the same time, without causing data races.

  3. 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.

  4. 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.

  5. 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 actors concepts are now clear.

You can reach out to me via Linked In or https://nasirmomin.web.app