Lesson 95Advanced Topics
Actors & Data Safety
What are Actors?
Actors are reference types like classes, but they protect their mutable state from data races. Only one task can access an actor's state at a time.
Actor vs Class
Class (Unsafe)
Concurrent access causes data races
Must manually synchronize
Actor (Safe)
Automatic isolation
Compiler-enforced safety
Key Concepts
actor
Isolated reference type
Access with await
nonisolated
Skip isolation for safe members
No await needed
@MainActor
Run on main thread
For UI updates
Sendable
Safe to cross boundaries
Value types are Sendable
Actor Isolation
actor Counter {
var count = 0
func increment() { count += 1 }
}
let c = Counter()
await c.increment() // Must await
var count = 0
func increment() { count += 1 }
}
let c = Counter()
await c.increment() // Must await
MainActor for UI
Use @MainActor for any code that updates UI. This ensures it runs on the main thread, preventing UI glitches and crashes.
@MainActor class ViewModel { ... }
main.swift
// ACTORS & DATA SAFETY
// Protect shared mutable state in concurrent code
// ===== THE PROBLEM: DATA RACES =====
// Without protection, concurrent access causes bugs
class UnsafeCounter {
var count = 0
func increment() {
count += 1 // Not thread-safe!
}
}
// ===== ACTORS: THE SOLUTION =====
actor SafeCounter {
var count = 0
func increment() {
count += 1 // Protected by actor
}
func getCount() -> Int {
return count
}
}
let counter = SafeCounter()
// Must use await to access actor methods/properties
Task {
await counter.increment()
let value = await counter.getCount()
print("Count: \(value)")
}
// ===== ACTOR ISOLATION =====
actor BankAccount {
let owner: String
private var balance: Double
init(owner: String, balance: Double) {
self.owner = owner
self.balance = balance
}
// Synchronous access inside actor
func deposit(_ amount: Double) {
balance += amount
}
func withdraw(_ amount: Double) -> Bool {
guard balance >= amount else { return false }
balance -= amount
return true
}
func getBalance() -> Double {
return balance
}
}
let account = BankAccount(owner: "Alice", balance: 1000)
Task {
await account.deposit(500)
let success = await account.withdraw(200)
print("Withdrawal success: \(success)")
print("Balance: \(await account.getBalance())")
}
// ===== NONISOLATED =====
// Properties/methods that don't need isolation
actor User {
let id: String // let is implicitly nonisolated
var name: String
init(id: String, name: String) {
self.id = id
self.name = name
}
// Doesn't access mutable state
nonisolated func getID() -> String {
return id // Can be called without await
}
func getName() -> String {
return name // Needs await
}
}
let user = User(id: "123", name: "Bob")
print(user.id) // No await needed
print(user.getID()) // No await needed
Task {
print(await user.getName()) // Needs await
}
// ===== MAINACTOR =====
// Ensures code runs on main thread (for UI)
@MainActor
class ViewModel {
var data: [String] = []
func updateUI() {
// Safe to update UI here
data.append("New item")
}
}
// Mark individual functions
actor DataManager {
var items: [String] = []
@MainActor
func updateUI(with newItems: [String]) {
// Runs on main thread
print("Updating UI with \(newItems.count) items")
}
func fetchData() async {
// Simulate fetch
try? await Task.sleep(nanoseconds: 1_000_000_000)
items = ["A", "B", "C"]
// Switch to main thread for UI
await updateUI(with: items)
}
}
// ===== SENDABLE =====
// Types that are safe to share between actors
// Value types are Sendable by default
struct Point: Sendable {
var x: Int
var y: Int
}
// Classes need @unchecked if you ensure thread safety
final class ThreadSafeCache: @unchecked Sendable {
private let lock = NSLock()
private var cache: [String: Any] = [:]
func set(_ value: Any, for key: String) {
lock.lock()
cache[key] = value
lock.unlock()
}
}
// ===== ASYNC SEQUENCES WITH ACTORS =====
actor MessageQueue {
private var messages: [String] = []
func send(_ message: String) {
messages.append(message)
}
func receive() -> String? {
return messages.isEmpty ? nil : messages.removeFirst()
}
func getAllMessages() -> [String] {
return messages
}
}
// ===== GLOBAL ACTORS =====
@globalActor
actor DatabaseActor {
static let shared = DatabaseActor()
}
@DatabaseActor
func saveToDatabase(_ data: String) {
print("Saving: \(data)")
}
// ===== ACTOR REENTRANCY =====
actor ImageLoader {
var cache: [String: Data] = [:]
func loadImage(url: String) async -> Data {
// Check cache first
if let cached = cache[url] {
return cached
}
// Suspension point - other calls can run here!
let data = await downloadImage(url: url)
// Check again after suspension
if let cached = cache[url] {
return cached
}
cache[url] = data
return data
}
func downloadImage(url: String) async -> Data {
try? await Task.sleep(nanoseconds: 500_000_000)
return Data()
}
}Try It Yourself!
Create a ShoppingCart actor with thread-safe add, remove, and total calculation methods!