Actor — 是 Swift 5.5+ 中引入的一种类型,它在语言层面解决了数据竞态问题。与手动锁和 DispatchQueue 队列不同,actor 自动隔离其状态并同步对它的访问。这意味着两个线程不能同时修改同一个 actor 类型的属性,从而无需开发者额外努力就排除了竞态条件。该机制基于 actor 隔离的概念,编译器跟踪对 actor 属性和方法的访问。据 Apple,2025 报道,actor 模型是 Swift 并发系统的基本组成部分。
主要内容
Actor — 是一种引用类型,类似于类,但具有自动的竞态保护。它是在 Swift 5.5 中作为并发系统的一部分,与 async/await 和 Task 一起引入的。Actor 保证其可变状态始终不会在没有显式同步的情况下从不同线程同时被读取或写入。
声明 actor 使用关键字 actor,后跟包含其成员的花括号。从语法上看,actor 类似于类,但行为或彻底不同。
actor BankAccount {
private var balance: Double
init(initialBalance: Double) {
self.balance = initialBalance
}
func deposit(amount: Double) {
balance += amount
}
func getBalance() -> Double {
return balance
}
}
编译器自动隔离 actor 的所有属性和方法,使它们只能在 actor 上下文内访问。从 actor 外部尝试访问 balance 将会导致编译器报错,除非调用被标记为 async。
数据隔离 — 是 actor 的关键概念。Actor 通过 actor 执行器(actor executor)机制保证对其状态的互斥访问。每个 actor 都有自己的执行器,它顺序处理对其隔离成员的所有访问。
当 actor 外部的代码调用其方法时,调用被放置在 actor 执行器的队列中。Actor 每次只执行一个任务,从而保证不会发生竞态。如果两个线程同时调用 deposit,第二个调用将等待第一个完成。
let account = BankAccount(initialBalance: 1000.0)
// 异步调用 — 从 actor 外部需要
await account.deposit(amount: 500.0)
let currentBalance = await account.getBalance()
每次调用 actor 方法都需要 await,因为 actor 可能正在处理其他任务。这不是 bug,而是一种有意识的设计,用于防止竞态。Swift 隐式地将 actor 属性的 getter 和 setter 变为 async,因此读取属性也需要 await。
Actor 和类都是引用类型,但它们在多线程环境中的行为截然不同。类不提供任何自动的竞态保护,而 actor 通过类型系统在编译器层面内置了这种保护。
| 特征 | Actor | Class |
|---|---|---|
| 竞态保护 | 自动,在编译器层面 | 需要手动同步 |
| 属性访问 | 只能通过 await 从外部 | 直接,无同步 |
| 继承 | 只能从其他 actor | 标准类继承 |
| 协议兼容 | 可以遵循协议 | 标准 |
| 性能 | 隔离时开销低 | 无同步 — 更快 |
Actor 只能从另一个 actor 继承,不能从类继承。这是有意为之的,因为类没有 actor 隔离机制,混合两种模型将导致未定义行为。
actor SavingsAccount: BankAccount {
func applyInterest(rate: Double) {
let interest = balance * rate
balance += interest
}
}
异步调用 — 是从外部代码与 actor 互动的机制。由于 actor 隔离其状态,从外部访问其成员都需要 await。这使得 Swift 能够保证调用代码不会阻塞线程,并且 actor 可以处理其他请求。
除了声明的 actor 类型外,Swift 还支持 全局 actor — @MainActor 属性,它将类、属性或方法标记为在主线程上执行。这在使用 UIKit 和 SwiftUI 时尤其有用。
@MainActor
class ViewModel: ObservableObject {
@Published var title: String = ""
func updateTitle() {
// 该代码保证在主线程上运行
title = "New Title"
}
}
使用 @MainActor 消除了手动调用 DispatchQueue.main.async 的必要,使代码更清洁、更安全。编译器检查切换到主线程是否正确发生。
Nonisolated — 是一个关键字,允许将 actor 的方法或计算属性标记为非隔离的。这些成员无法访问 actor 的隔离状态,但可以从 actor 外部无需 await 就能调用。
Nonisolated 方法对于不依赖于 actor 可变状态的计算很有用。例如,formatBalance 方法不直接访问 balance,只是格式化传入的值 — 这样的方法可以安全地设为 nonisolated。
actor BankAccount {
private var balance: Double = 0
nonisolated func formatBalance(amount: Double) -> String {
return "$\(amount)"
}
}
Nonisolated 成员同步执行,不需要 await。但它们不能直接读取 actor 的隔离属性。如果 nonisolated 方法需要来自 actor 的值,则必须将其 作为参数传递。
Reentrancy — 是一种机制,允许在等待异步调用时重新进入 actor。如果没有 reentrancy,当 actor 的一个方法等待另一个,而另一个又回过头等待第一个时,actor 可能会永久锁死。
当 actor 内部的代码执行 await 时,actor 暂停当前任务,并可以 处理另一个排队的任务。await 完成后,任务恢复执行。这防止了死锁,但需要谨慎: actor 在 await 点之间的状态可能会改变。
actor DataProcessor {
var cache: [Int: String] = [:]
func process(id: Int) async -> String {
if let cached = cache[id] {
return cached
}
// await — 重新进入点
let result = await fetchData(id: id)
// 缓存可能在 await 之后发生了变化 — 请重新检查
cache[id] = result
return result
}
}
开发者必须考虑 reentrancy,并在 await 点之后检查 actor 的状态。一个典型的错误是假设 actor 隔离在异步 暂停期间保持不变。实际上,在 await 和下一条语句之间,状态可能与预期的不同。
常见问题
Actor 自动将其状态与数据竞态隔离,从外部访问需要 await。类不提供这种保护 — 开发者自行负责通过锁或队列进行同步。Actor 只能从 actor 继承,类从类继承。
可以,actor 可以从另一个 actor 继承。子类继承父类的所有隔离属性和方法。Actor 不能从类继承,因为类在编译器层面没有 actor 隔离机制。
Actor-isolated 上下文 — 是允许直接访问 actor 可变状态的代码区域。在被标记为 isolated(默认)的 actor 方法内部,可以无需 await 就读写属性。编译器检查隔离边界。
来自 actor 的数据通过 async 方法(返回 Sendable 类型)或通过接受值作为参数的 nonisolated 方法传递。还可以创建一个 async 属性,它以 Sendable 结构的形式返回 actor 状态的快照。
可以,actor 可以遵循协议。如果协议包含 隔离要求(actor-isolated),它们将自动成为 actor-隔离的。对于协议中的异步方法,可以使用 isolated 标记指定它们应在特定的 actor 上调用。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。