actor浅析

736 阅读11分钟

actor浅析

1. Actor 的定义

  • Actor 是 Swift 5.5 引入的并发模型核心类型,用于解决多线程环境下的数据竞争(Data Race)问题。
  • 它通过 数据隔离(Isolation 确保对共享状态的访问是串行的,从而避免并发冲突。
  • 类型特性:引用类型(引用语义),但通过编译器强制隔离保护内部状态。
actor BankAccount {
    private var balance: Double = 0.0

    func deposit(amount: Double) {
        balance += amount
    }

    func withdraw(amount: Double) async -> Bool {
        if balance >= amount {
            balance -= amount
            return true
        }
        return false
    }
}

2. Actor 的核心特性

2.1 数据隔离(Isolation)

  • 规则:只有 Actor 自身的方法(或 nonisolated 方法)可以直接访问其内部状态。
  • 外部访问:必须通过 await 异步调用,确保访问发生在 Actor 的串行队列中。
  • 编译器强制检查:Swift 编译器会阻止外部直接访问 Actor 的属性和方法(除非标记为 nonisolated)。
let account = BankAccount()
Task {
    await account.deposit(amount: 100) // 必须异步调用
}

2.2 可重入性(Reentrancy)

2.2.1. Reentrancy 的核心概念
  • 定义:Reentrancy 允许 actor 在异步方法挂起(如遇到 await)时,暂时让出对其状态的独占访问权。其他任务可以在此期间调用该 actor 的方法,避免阻塞。
  • 目的:提高并发性能,避免死锁,同时保持 actor 状态的安全性。
2.2.2. Reentrancy 的工作流程

假设一个 actor 方法执行如下操作:

  1. 同步执行:在遇到 await 之前,代码是同步执行的,此时 actor 独占其状态。
  2. 异步挂起:在 await 处,当前任务挂起,actor 释放对其状态的独占。
  3. 其他任务介入:在挂起期间,其他任务可以调用该 actor 的方法。
  4. 恢复执行:当 await 的异步操作完成后,actor 重新获得状态访问权,继续执行后续代码。
actor BankAccount {
    var balance: Double = 0

    func withdraw(_ amount: Double) async {
        guard balance >= amount else {
            print("余额不足,等待存款...")
            await Task.sleep(1_000_000_000) // 模拟异步等待
            // ⚠️ 注意:此时其他任务可能修改了 balance!
            guard balance >= amount else {
                print("仍然余额不足")
                return
            }
        }
        balance -= amount
        print("成功取款 \(amount)")
    }

    func deposit(_ amount: Double) {
        balance += amount
        print("存入 \(amount)")
    }
}

2.2.3. Reentrancy 的注意事项
潜在问题
  • 状态可能在挂起后改变:在 await 挂起期间,其他任务可能修改了 actor 的状态,导致恢复后的代码逻辑假设失效。
    • 例如,在 withdraw 方法中,可能在 await 之后余额被其他任务修改,需要重新验证条件。
解决方案
  • 始终重新验证状态:在 await 之后,必须重新检查所有前置条件(如余额是否足够)。
  • 避免依赖挂起前的临时状态:不要在挂起后假设任何状态未变。

2.2.4. Reentrancy 的优势
  1. 更高的并发性:避免因长时间独占 actor 状态而阻塞其他任务。
  2. 防止死锁:如果 actor 方法需要调用其他 actor 的方法,Reentrancy 允许这些调用交错执行,避免相互等待。
  3. 更自然的异步代码:允许在 actor 方法中组合多个异步操作。

2.2.5. 如何正确使用 Reentrancy
最佳实践
  1. 将可变状态访问限制在同步代码块:在 await 之前完成所有状态修改。
  2. 避免副作用:确保 await 前后的代码不依赖未重新验证的临时状态。
  3. 使用不可变数据:在异步操作中传递不可变数据(如结构体或 Sendable 类型)。
错误示例与修复
// ❌ 错误:假设在 await 后余额未变
func withdraw(_ amount: Double) async {
    if balance >= amount {
        await someAsyncOperation() // 挂起期间 balance 可能被修改
        balance -= amount // 可能不安全!
    }
}

// ✅ 修复:重新验证状态
func withdraw(_ amount: Double) async {
    if balance >= amount {
        await someAsyncOperation()
        // 重新检查条件
        if balance >= amount {
            balance -= amount
        }
    }
}

2.3 可发送类型(Sendable)

2.3.1 Sendable 的定义与目的
  • 作用:声明某个类型的数据可以在并发环境中安全共享,即该类型的所有权可以跨线程或跨 Actor 传递。
  • 核心目标:防止数据竞争(确保共享数据的不可变性或线程安全)。
  • 适用场景
    • Taskasync let 或 Actor 之间传递数据。
    • 将闭包标记为 @Sendable,用于跨并发域执行。
struct User: Sendable {
    let id: String
    let name: String
}

// 跨 Task 安全传递
Task {
    let user = User(id: "123", name: "Alice")
    await processUser(user) // ✅ 安全
}

2.3.2. 遵循 Sendable 的条件

只有满足以下条件的类型可以安全遵循 Sendable

  1. 值类型(结构体、枚举)
  • 默认遵循:所有存储属性必须为 Sendable 类型。
  • 无需显式声明:如果所有属性都是 Sendable,编译器会自动推断。
// 自动推断为 Sendable(所有属性均为 Sendable)
struct Point: Sendable {
    var x: Double
    var y: Double
}

// 错误示例:包含非 Sendable 属性 ❌
struct UserProfile {
    var name: String
    var settings: NSMutableDictionary // 非 Sendable(可变引用类型)
}
  1. Actor 类型
  • 自动遵循:所有 actor 类型隐式遵循 Sendable,因为它们内部的状态通过隔离保护。
actor BankAccount { /* ... */ }

func transfer(account: BankAccount) { // ✅ 安全
    Task { await account.deposit(amount: 100) }
}
  1. 类(Class)
  • 严格限制:只有不可变(immutable)的 final 类可以显式声明为 Sendable
    • 所有存储属性必须为常量(let)且类型为 Sendable
    • 类本身必须标记为 final,禁止继承。
// 正确示例 ✅
final class AppConfig: Sendable {
    let apiURL: String
    init(apiURL: String) { self.apiURL = apiURL }
}

// 错误示例 ❌
class UserManager: Sendable { // 非 final,可能被继承
    var lastUpdated = Date() // 可变属性
}
2.3.3 @Sendable 闭包
  • 用途:标记闭包可以在并发域之间传递。
  • 要求:闭包必须:
    • 不捕获非 Sendable 的变量。
    • 所有捕获的变量必须是 Sendable 类型或不可变值。
func runAsyncTask() {
    var counter = 0 // 非 Sendable 的局部变量
    Task {
        // 错误:闭包捕获了可变的局部变量 ❌
        await someActor.updateCount { counter += 1 }
    }
}

// 正确示例 ✅
func safeTask() {
    let maxRetries = 3 // 不可变值(Sendable)
    Task {
        await someActor.retryOperation(max: maxRetries)
    }
}

2.3.4 编译器检查与手动处理
  • 编译器强制检查:在跨并发域传递数据时,如果类型不满足 Sendable,编译器会报错。
  • 手动标记为 Sendable:若确定类型是线程安全的,但编译器无法推断,可强制标记(需谨慎!)。
// 强制标记(需自行确保线程安全)
final class UnsafeCounter: @unchecked Sendable {
    private var count = 0
    func increment() { // 手动通过锁保护
        lock.lock()
        defer { lock.unlock() }
        count += 1
    }
    private let lock = NSLock()
}

2.3.5 常见问题与陷阱
  1. Sendable 类型传递
class Logger { /* 非 Sendable */ }

Task {
    let logger = Logger()
    await logMessage(logger) // ❌ 编译错误:Logger 非 Sendable
}
  1. 闭包捕获可变状态
var globalCounter = 0 // 全局变量(非 Sendable)

Task {
    await someActor.update { globalCounter += 1 } // ❌ 不安全
}
  1. 强制绕过检查的风险
// 可能引发数据竞争!
let unsafeArray = NSMutableArray()
Task {
    await someActor.modifyArray(unsafeArray as! Sendable) // ⚠️ 强制转换危险
}
2.3.6 实际应用场景
  • 跨 Actor 传递数据:确保参数和返回值是 Sendable
  • Task 间共享配置:例如传递只读的配置对象。
  • 并行集合处理:使用 withTaskGroup 时,元素需为 Sendable
func processUsers(users: [User]) async {
    await withTaskGroup(of: Void.self) { group in
        for user in users { // User 需为 Sendable
            group.addTask { await processUser(user) }
        }
    }
}
2.3.7. Sendable 总结
  • 关键规则
    • 值类型(结构体、枚举)通常自动遵循 Sendable
    • Actor 类型隐式遵循 Sendable
    • 类必须严格满足不可变条件才能标记为 Sendable
    • 闭包需用 @Sendable 并避免捕获可变状态。
  • 最佳实践
    • 优先使用值类型和 Actor。
    • 避免在并发代码中使用可变引用类型。
    • 谨慎使用 @unchecked Sendable,确保手动实现线程安全。

3. Actor 的类型

3.1. 普通 Actor(Normal Actor)

3.1.1 定义与核心特性
  • 定义:通过 actor 关键字声明的自定义类型,用于隔离和保护其内部状态。
  • 作用范围:每个普通 Actor 是一个独立的实例,管理自己的串行队列。
  • 数据隔离:只有 Actor 内部的方法可以直接访问其属性(或标记为 nonisolated 的方法)。
  • 跨线程安全:外部访问必须通过 await 异步调用,确保线程安全。
// 定义一个普通 Actor
actor Counter {
    private var count = 0
    
    func increment() {
        count += 1
    }
    
    func currentValue() -> Int {
        return count
    }
}

// 使用示例
let counter = Counter()
Task {
    await counter.increment()
    let value = await counter.currentValue()
    print(value) // 输出 1
}
3.1.2 使用场景
  • 共享资源的串行访问:例如计数器、缓存、网络请求队列。
  • 替代锁机制:比 DispatchQueueNSLock 更安全,编译器强制检查。
3.1.3 生命周期
  • 引用类型:普通 Actor 是引用类型,通过 ARC 管理内存。
  • 弱引用:可通过 weakunowned 避免循环引用。
class ViewModel {
    weak var counter: Counter? // 弱引用避免循环
}

3.2. 全局 Actor(Global Actor)

3.2.1 定义与核心特性
  • 定义:全局 Actor 是一种特殊的 Actor,用于标记某些代码必须在特定的全局上下文中执行(如主线程)。
  • 隐式单例:全局 Actor 通常只有一个共享实例(如 @MainActor)。
  • 强制上下文:标记为全局 Actor 的函数或属性,必须在对应的 Actor 上下文中调用。
// 使用 @MainActor 确保在主线程执行
@MainActor
func updateUI() {
    // 修改 UI 控件(如 UILabel、UIButton)
}

// 在非主线程调用会触发编译器错误 ❌
Task {
    updateUI() // 错误:必须用 await 或标记为 @MainActor
}

// 正确调用 ✅
Task { @MainActor in
    updateUI()
}

// 或者在普通 Actor 中切换上下文
actor DataLoader {
    func loadData() async {
        let data = await fetchData()
        await MainActor.run { // 切换到主线程
            updateUI(with: data)
        }
    }
}
3.2.2 常见全局 Actor
  • @MainActor:最常用的全局 Actor,用于 UI 更新。
  • 自定义全局 Actor:可创建自己的全局 Actor(如 @DatabaseActor)。
// 自定义全局 Actor
@globalActor
actor DatabaseActor {
    static let shared = DatabaseActor()
}

// 标记为在 DatabaseActor 上下文中执行
@DatabaseActor
func saveToDatabase(data: Data) {
    // 数据库操作
}
3.2.3 使用场景
  • UI 操作:所有 UIKit/SwiftUI 的更新必须在 @MainActor 中执行。
  • 资源单例:如数据库连接、文件管理器等需要全局串行访问的资源。

3.3 普通 Actor vs 全局 Actor 对比

特性普通 Actor全局 Actor
声明方式通过 actor 关键字定义类型通过 @globalActor 标记协议或类型
实例化可创建多个实例通常是单例(如 @MainActor
数据隔离范围实例级别的隔离全局级别的隔离(跨整个应用)
典型用途管理特定共享资源(如银行账户)主线程操作、全局单例资源访问
上下文切换通过 await 显式切换通过 @ActorName 隐式或显式切换

3.4 全局 Actor 的高级用法

3.4.1 将整个类型标记为全局 Actor
@MainActor
class ViewController: UIViewController {
    // 所有方法和属性默认在主线程执行
    func updateLabel() {
        label.text = "Updated" // 无需额外切换
    }
}
3.4.2 选择性脱离全局 Actor

使用 nonisolated 标记不需要隔离的方法:

错误示例:

@MainActor
class DataModel {
    private var data: [String] = []
    
    // 此方法在主线程执行
    func addData(_ item: String) {
        data.append(item)
    }
    
    // 脱离 MainActor 隔离(但需确保线程安全)
    nonisolated func getDataCount() -> Int {
        return data.count // 错误!无法直接访问隔离属性 ❌
    }
}

正确示例:

@MainActor
class DataModel {
    private let data: [String] = []  // 改为不可变属性
    
    nonisolated func safeGetCount() -> Int {
        return data.count  // ✅ 安全:data 是不可变的 Sendable 类型
    }
}

nonisolated 的正确用法

  1. 访问非隔离的 Sendable 数据 若方法不依赖 Actor 的隔离状态,可以直接标记为 nonisolated
@MainActor
class UserProfile {
    private var name: String  // 隔离属性
    let userId: String        // 非隔离属性(常量 + Sendable)
    
    nonisolated func getUserId() -> String {
        return userId  // ✅ 安全:访问的是不可变的 Sendable 属性
    }
}
  1. 纯计算或协议实现 例如实现 HashableEquatable 协议时,若逻辑不依赖隔离状态:
@MainActor
class DataItem: Hashable {
    private var id: UUID
    
    // 必须用 nonisolated:协议方法不能是异步的
    nonisolated func hash(into hasher: inout Hasher) {
        hasher.combine(id)  // 假设 id 是 Sendable(UUID 是值类型)
    }
    
    static func == (lhs: DataItem, rhs: DataItem) -> Bool {
        lhs.id == rhs.id   // 同样需要 nonisolated 标记
    }
}

nonisolated 与线程安全的深层关系

  • 黄金规则nonisolated 方法中访问的所有数据必须满足以下条件之一:
    1. 不可变且 Sendable(如 let 常量、值类型)。
    2. 显式线程安全(如通过锁、原子操作)。
    3. 属于其他 Actor 的隔离状态(需通过 await 调用其方法访问)。

3.5. actor注意事项

  1. 避免阻塞操作:在 Actor 内部避免同步阻塞调用(如 sleep),否则会阻塞整个 Actor 队列。
  2. 死锁风险:多个 Actor 相互等待可能导致死锁。
  3. 合理使用 nonisolated:标记为 nonisolated 的方法无法访问隔离状态。
  4. 全局 Actor 的性能:频繁切换全局 Actor(如 @MainActor)可能影响性能。

3.6. 实际应用示例

场景 1:普通 Actor 管理缓存
actor ImageCache {
    private var cache: [URL: UIImage] = [:]
    
    func getImage(for url: URL) -> UIImage? {
        return cache[url]
    }
    
    func setImage(_ image: UIImage, for url: URL) {
        cache[url] = image
    }
}
场景 2:全局 Actor 处理数据库
@globalActor
actor DatabaseActor {
    static let shared = DatabaseActor()
}

@DatabaseActor
class DatabaseManager {
    func save(_ data: Data) {
        // 串行化数据库写入
    }
}

// 使用示例
Task {
    let data = Data(...)
    await DatabaseManager().save(data) // 在 DatabaseActor 上下文中执行
}

3.7 actor总结

  • 普通 Actor:用于保护实例级别的共享状态,通过 actor 类型实现。
  • 全局 Actor:用于全局上下文管理(如主线程),通过 @globalActor 标记。
  • 核心区别:普通 Actor 是实例级别的隔离,全局 Actor 是应用级别的单例隔离。