2-15.【Concurrency】actor 内部属性在并发访问时是否安全?为什么?

4 阅读2分钟

1️⃣ 结论

Actor 内部的状态(属性)是串行化访问的,只允许通过 Actor 的执行上下文访问,因此不需要额外锁,天然线程安全。


2️⃣ Actor 如何保证安全

① 单一执行上下文(Executor)

  • 每个 Actor 都有自己的 executor,类似一个队列。
  • 同一时间只能有一个任务在执行 Actor 的方法或访问属性
  • 多个任务同时访问 Actor 状态时,会排队顺序执行。
Actor BankAccount (executor)
 ├─ Task 1 accessing balance
 ├─ Task 2 queued
 └─ Task 3 queued
  • Task 2、3 必须等 Task 1 完成,保证状态访问不会并发冲突。

② async 消息传递

  • 跨 Actor 调用必须使用 await
await account.deposit(100)
  • 调用被封装成一个任务,放入 Actor 的 executor 队列。
  • 即使多个调用同时发起,也会排队执行,保证 属性修改互不干扰

③ Actor 内部同步

  • Actor 内部方法可以直接访问属性,无需加锁。
  • 只有跨 Actor 调用才是异步的。
actor Counter {
    private var value = 0
    
    func increment() {
        value += 1 // 安全
    }
    
    func getValue() -> Int {
        value // 安全
    }
}
  • 即使多个 Task 同时调用 increment,Swift runtime 会排队执行,保证线程安全。

3️⃣ 对比传统锁机制

特性Actor锁(Mutex / NSLock)
并发安全✅ 内部属性串行访问✅ 需要手动加锁
死锁风险❌ 消息队列顺序执行✅ 高风险
可组合性✅ async/await 天然支持❌ 异步组合复杂
可推理性

Actor 通过“串行化访问 + 异步消息”天然消除了数据竞争。


4️⃣ 小陷阱

  1. 跨 Actor 调用必须 await

    • 否则编译器报错,避免直接并发访问状态。
actor BankAccount {
    var balance = 0
}

let account = BankAccount()
account.balance += 10 // ❌ 编译错误
await account.balance // ✅ 正确
  1. Actor 内部可以安全同步

    • Actor 内部方法和属性可以直接访问,不需锁。
  2. Actor 内部调用跨 Actor 仍需注意顺序

    • 如果两个 Actor 相互调用,可能需要设计好访问顺序,否则可能死锁或延迟。

5️⃣ 面试级总结

Swift Actor 的内部属性是线程安全的,因为 Actor 保证了同一时间只有一个任务访问其状态。它通过 executor 串行化所有任务,并要求跨 Actor 调用使用 async/await,从而实现天然的数据隔离和并发安全。相比传统锁,Actor 更易推理、更安全,也更易组合异步任务。