Concurrency using Actor

5 minute read

Overview

  • Kind object type, on a par with enum, class, struct
  • Actor is a reference type, similar to a class, can inherit from other actors, conforms to protocol, be generics, and be used with generics
  • Difference is actor has isolation domain, which helps in protecting mutable state, prevent data races
  • Actor’s code run on background by default

Actor Isolation

  1. If an actor instance have mutable property (var), can only be modified on self
  2. Can access actor’s property, but this access is asynchronous, must use await
  3. Can call actor’s method, but this access is asynchronous, must use await (even if it is not an async method)

Why await❓ To make sure, no other code belonging to the same actor can start, solves the problem of simultaneous access. With an actor, there is no simultaneous access. Access to an actor is serialized. ✅

 1actor BankAccount {
 2    let accountNumber: Int
 3    var balance: Double
 4    
 5    init(accountNumber: Int, balance: Double) {
 6        self.accountNumber = accountNumber
 7        self.balance = balance
 8    }
 9}
10
11let accountA = BankAccount(accountNumber: 1, balance: 100.0)
12print(accountA.accountNumber) // ✅ constant value, so can be directly accessible
13print(accountA.balance) // ⛔️ mutable property, have to put await
14
15Task {
16    print(await accountA.balance) // ✅ it's legal now
17}
18
19accountA.balance = 200.0 // ⛔️ this is illegal, will trigger an error as cannot modify not on self

Let’s add a method to the actor 👍 See, in transfer(amount:to), we reference to an actor → this is called cross-actor reference

1extension BankAccount {
2    @discardableResult
3    func transfer(amount: Double, to other: BankAccount) -> Bool {
4        guard amount > 0 && amount <= balance else { return false }
5        self.balance -= amount // ✅ legal, balance is modified on self
6        other.balance += amount // illegal, not on self ➡️ modified on non-isolated actor instance
7        return true
8    }
9}

Solution: Add new method to the actor, say deposit() that legally modify balance

 1extension BankAccount {
 2    func deposite(_ amount: Double) {
 3        self.balance += amount
 4    }
 5    
 6    @discardableResult
 7    func transfer(amount: Double, to other: BankAccount) async -> Bool {
 8        guard amount > 0 && amount <= balance else { return false }
 9        self.balance -= amount
10        await other.deposite(amount) // ✅ legal now
11        return true
12    }
13}

Another one: use isolated keyword isolated will bind method transfer(amount:to) to other isolation domain

1extension BankAccount {
2    @discardableResult
3    func transfer(amount: Double, to other: isolated BankAccount) -> Bool {
4        guard amount > 0 && amount <= balance else { return false }
5        self.balance -= amount
6        other.balance += amount
7        return true
8    }
9}

Sendable types

  • Values of types that conform to Sendable protocol are safe to share accross concurrency domain:
    • value-semantic type (Int, Bool, Double, String…)
    • value-semantic collections ([Int], [Int: String])
    • immutable class (only containing constant value let)
    • class that perform its own synchronization internally
    • structure or enum contains values of type that is Sendable will implicitly Sendable
  • Actor type implicitly conforms to Sendable protocol, protects its mutable state, actor instances can be safe to share accross concurrency domain.

⚠️ Issue with Actor:

  • Add a list of owners to the actor, owner in type of Person
 1// This is not implicitly conforming to Sendable
 2// Since it is a class - reference type 
 3// have a mutable property which can be mutated on multiple threads
 4class Person {
 5    var name: String
 6    let age: Int
 7    
 8    init(name: String, age: Int) {
 9        self.name = name
10        self.age = age
11    }
12}
13
14actor BankAccount {
15    ...
16    var owners: [Person]
17    ...
18}
  • Add a getter function to return primary owner of the account
1extension BankAccount {
2    ...
3    func primaryOwner() -> Person? {
4        owners.first
5    }
6}
  • Now, time to test…
1let primaryOwnerOfAccountA = Person(name: "Aaron Phan", age: 24)
2let accountA = BankAccount(accountNumber: 1, balance: 100.0, owners: [primaryOwnerOfAccountA])
3
4Task {
5    if let primaryOwner = await accountA.primaryOwner() {
6        primaryOwner.name = "Phan Vu Tinh" // ⛔️ should be avoid since `name` can be mutated on multiple threads but XCode does not warn it
7    }
8}

The problem is that we still can access the actor property (which is not Sendable) and modify one of its properties on multiple thread without any complain

Solution:

  • mark Person as final to stop further inheritance
  • make it conform to Sendable with @unchecked
  • encapsolate name, create updateName() to update name with serial queue
 1final class Person: @unchecked Sendable {
 2    private var name: String
 3    let age: Int
 4    private let serialQueue = DispatchQueue(label: "person.serial.queue")
 5    
 6    init(name: String, age: Int) {
 7        self.name = name
 8        self.age = age
 9    }
10    
11    func updateName(_ name: String) {
12        serialQueue.sync {
13            self.name = name
14        }
15    }
16}

Main Actor

  • Isolate code to main actor, it will run on main thread
  • How? Use @MainActor directive on:
    • property (static/class/instance property) 👉 only be accessed on main thread
    • closure, method (static/class/instance property) 👉 only be called on main thread
    • a type (class, struct, enum) 👉 all of its properties and methods are only accessed only on the main thread.
    • global variable or function
 1actor MyActor {
 2    
 3    let id = UUID().uuidString
 4    @MainActor var actorProperty: String
 5    
 6    init(actorProperty: String) {
 7        self.actorProperty = actorProperty
 8    }
 9    
10    @MainActor func mutateProperty(_ newValue: String) {
11        self.actorProperty = newValue
12    }
13}
14
15print(actor.actorProperty) // ✅ now can be access it on Main thread without `await`
16actor.mutateProperty("Vietnam") // ✅ it is legal 
17actor.actorProperty = "Hochiminh city" // ✅ it is legal, can be mutate directly

⁉️ Switching context from background thread to main thread for updating UI? MainActor has an extension

1@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
2extension MainActor {
3
4    /// Execute the given body closure on the main actor.
5    public static func run<T>(resultType: T.Type = T.self, body: @MainActor @Sendable () throws -> T) async rethrows -> T
6}

Usage:

 1let imgUrl = "https://cdn.pixabay.com/photo/2015/04/23/22/00/tree-736885__480.jpg"
 2Task.detached {
 3    // 🅱️ Run on background thread 
 4    do {
 5        let data = try await self.download(url: URL(string: imgUrl)!)
 6        await MainActor.run {
 7        // Ⓜ️ Now, switched to Main thread
 8            self.image1.image = UIImage(data: data)
 9        }
10    } catch(let error) {
11        print(error.localizedDescription)
12    }
13}