Concurrency using Actor
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
- If an actor instance have mutable property (
var
), can only be modified onself
- Can access actor’s property, but this access is asynchronous, must use
await
- Can call actor’s method, but this access is asynchronous, must use
await
(even if it is not anasync
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 implicitlySendable
- value-semantic type (
- 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
asfinal
to stop further inheritance - make it conform to
Sendable
with@unchecked
- encapsolate
name
, createupdateName()
to updatename
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}