首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

actor浅析

  • 25-04-18 23:46
  • 3439
  • 11666
juejin.cn

actor浅析

1. Actor 的定义

  • Actor 是 Swift 5.5 引入的并发模型核心类型,用于解决多线程环境下的数据竞争(Data Race)问题。
  • 它通过 数据隔离(Isolation 确保对共享状态的访问是串行的,从而避免并发冲突。
  • 类型特性:引用类型(引用语义),但通过编译器强制隔离保护内部状态。
swift
代码解读
复制代码
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)。
swift
代码解读
复制代码
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 重新获得状态访问权,继续执行后续代码。
swift
代码解读
复制代码
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 类型)。
错误示例与修复
swift
代码解读
复制代码
// ❌ 错误:假设在 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 传递。
  • 核心目标:防止数据竞争(确保共享数据的不可变性或线程安全)。
  • 适用场景:
    • 在 Task、async let 或 Actor 之间传递数据。
    • 将闭包标记为 @Sendable,用于跨并发域执行。
swift
代码解读
复制代码
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,编译器会自动推断。
swift
代码解读
复制代码
// 自动推断为 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,因为它们内部的状态通过隔离保护。
swift
代码解读
复制代码
actor BankAccount { /* ... */ } func transfer(account: BankAccount) { // ✅ 安全 Task { await account.deposit(amount: 100) } }
  1. 类(Class)
  • 严格限制:只有不可变(immutable)的 final 类可以显式声明为 Sendable。
    • 所有存储属性必须为常量(let)且类型为 Sendable。
    • 类本身必须标记为 final,禁止继承。
swift
代码解读
复制代码
// 正确示例 ✅ 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 类型或不可变值。
swift
代码解读
复制代码
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:若确定类型是线程安全的,但编译器无法推断,可强制标记(需谨慎!)。
swift
代码解读
复制代码
// 强制标记(需自行确保线程安全) 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 类型传递
swift
代码解读
复制代码
class Logger { /* 非 Sendable */ } Task { let logger = Logger() await logMessage(logger) // ❌ 编译错误:Logger 非 Sendable }
  1. 闭包捕获可变状态
swift
代码解读
复制代码
var globalCounter = 0 // 全局变量(非 Sendable) Task { await someActor.update { globalCounter += 1 } // ❌ 不安全 }
  1. 强制绕过检查的风险
swift
代码解读
复制代码
// 可能引发数据竞争! let unsafeArray = NSMutableArray() Task { await someActor.modifyArray(unsafeArray as! Sendable) // ⚠️ 强制转换危险 }
2.3.6 实际应用场景
  • 跨 Actor 传递数据:确保参数和返回值是 Sendable。
  • Task 间共享配置:例如传递只读的配置对象。
  • 并行集合处理:使用 withTaskGroup 时,元素需为 Sendable。
swift
代码解读
复制代码
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 异步调用,确保线程安全。
swift
代码解读
复制代码
// 定义一个普通 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 使用场景
  • 共享资源的串行访问:例如计数器、缓存、网络请求队列。
  • 替代锁机制:比 DispatchQueue 或 NSLock 更安全,编译器强制检查。
3.1.3 生命周期
  • 引用类型:普通 Actor 是引用类型,通过 ARC 管理内存。
  • 弱引用:可通过 weak 或 unowned 避免循环引用。
swift
代码解读
复制代码
class ViewModel { weak var counter: Counter? // 弱引用避免循环 }

3.2. 全局 Actor(Global Actor)

3.2.1 定义与核心特性
  • 定义:全局 Actor 是一种特殊的 Actor,用于标记某些代码必须在特定的全局上下文中执行(如主线程)。
  • 隐式单例:全局 Actor 通常只有一个共享实例(如 @MainActor)。
  • 强制上下文:标记为全局 Actor 的函数或属性,必须在对应的 Actor 上下文中调用。
swift
代码解读
复制代码
// 使用 @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)。
swift
代码解读
复制代码
// 自定义全局 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
swift
代码解读
复制代码
@MainActor class ViewController: UIViewController { // 所有方法和属性默认在主线程执行 func updateLabel() { label.text = "Updated" // 无需额外切换 } }
3.4.2 选择性脱离全局 Actor

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

错误示例:

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

正确示例:

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

nonisolated 的正确用法

  1. 访问非隔离的 Sendable 数据 若方法不依赖 Actor 的隔离状态,可以直接标记为 nonisolated:
swift
代码解读
复制代码
@MainActor class UserProfile { private var name: String // 隔离属性 let userId: String // 非隔离属性(常量 + Sendable) nonisolated func getUserId() -> String { return userId // ✅ 安全:访问的是不可变的 Sendable 属性 } }
  1. 纯计算或协议实现 例如实现 Hashable 或 Equatable 协议时,若逻辑不依赖隔离状态:
swift
代码解读
复制代码
@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 管理缓存
swift
代码解读
复制代码
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 处理数据库
swift
代码解读
复制代码
@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 是应用级别的单例隔离。
注:本文转载自juejin.cn的Mr_zheng的文章"https://juejin.cn/post/7477874773252096009"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

141
iOS
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2024 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top