ARC 的线程安全处理是一个关于权衡的设计。Swift 的核心目标是:保证引用计数操作的绝对安全,但不保证对象内容访问的安全。
为了实现这一点,Swift 在底层采用了以下几种机制:
1. 原子性操作 (Atomic Operations)
引用计数的增加(retain)和减少(release)本身是原子性的。
- 底层实现:在 ARM64 架构下,Swift 运行时使用
ldxr(Load-exclusive) 和stxr(Store-exclusive) 指令对对象头部的引用计数位域进行操作。 - 效果:当多个线程同时尝试持有(retain)同一个对象时,引用计数的累加是正确的,不会出现因为竞态条件(Race Condition)导致的内存提前释放或泄漏。
2. 线程安全的“侧表” (Side Table)
当对象因为存在弱引用(Weak Reference)而需要 Side Table 时,侧表的创建过程也是线程安全的。
- 双重检查锁 (Double-checked locking) :Swift 确保在多个线程同时为一个对象创建侧表时,只有一个线程能成功。其他线程会发现侧表已存在,并直接链接到已有的侧表。
- 原子交换:通过原子交换指令将对象头的标志位从“内联计数”切换为“侧表指针”。
3. ARC 的局限:什么是不安全的?
这是一个常见的误区:ARC 保证了引用的安全性,但并不保证变量赋值的安全性。
-
指针赋值的竞态:
如果线程 A 正在将变量
x指向对象O1,而线程 B 同时将x指向对象O2。由于x本身只是一个指针,对x的赋值操作不是原子的。此时可能发生:线程 A 完成了对
O1的release,但在retainO2之前,线程 B 修改了x。这会导致引用计数的非对称操作,最终引发Double Free(双重释放)或内存崩溃。 -
解决办法:如果你需要在多线程间频繁读写同一个对象引用,必须使用锁(Lock)、信号量(Semaphore)或 Swift 现代并发中的 Actor。
4. 现代并发的终极方案:Actor
为了解决上述“指针赋值不安全”的问题,Swift 引入了 Actor 模型。
- 隔离性:Actor 保证了其内部状态(包括所有引用类型的属性)在同一时间只能被一个线程访问。
- 消除冗余 retain/release:在 Actor 内部处理逻辑时,由于编译器知道环境是单线程执行的(串行化),它可以优化掉大量的原子性引用计数操作,从而提升性能。
5. 性能与线程安全的权衡
因为 retain 和 release 必须是原子的,所以它们比非原子操作要慢(需要同步 CPU 缓存)。
- 编译器优化:Swift 编译器会进行 ARC 消除(ARC Elision) 。如果它能证明一个对象在某个作用域内不会消失,它会移除多余的原子操作指令。
- Local Objects:对于只在函数内部使用的对象,编译器会尽量避免触发涉及跨核同步的原子指令。
总结
| 处理对象 | 是否线程安全 | 实现机制 |
|---|---|---|
| 引用计数加减 | 是 | 原子指令 (LDR/STR Exclusive) |
| 侧表创建 | 是 | 原子交换与状态位检查 |
| 变量/指针赋值 | 否 | 需要手动加锁或使用 Actor |
| 对象属性修改 | 否 | 需要手动加锁或使用 Actor |
英文版
8-4. [Memory Management] How does ARC handle Thread Safety?
The thread safety of ARC is a design of trade-offs. Swift’s core objective is: To guarantee absolute safety for reference counting operations, but not for the access of the object's content itself.
To achieve this, Swift employs several low-level mechanisms:
1. Atomic Operations
The incrementing (retain) and decrementing (release) of reference counts are inherently atomic.
- Underlying Implementation: On ARM64 architecture, the Swift runtime uses
ldxr(Load-exclusive) andstxr(Store-exclusive) instructions to perform operations on the reference count bitfield within the object header. - The Result: When multiple threads simultaneously attempt to retain the same object, the cumulative reference count remains accurate. This prevents memory leaks or premature deallocation caused by race conditions.
2. Thread-Safe "Side Table" Creation
When an object requires a Side Table (due to the creation of a weak reference), the process of generating that table is also thread-safe.
- Double-Checked Locking: Swift ensures that if multiple threads attempt to create a Side Table for the same object at once, only one succeeds. Others will detect that the table already exists and link to it accordingly.
- Atomic Exchange: The runtime uses atomic exchange instructions to flip the object header's flag from "Inline Count" to "Side Table Pointer."
3. The Limits of ARC: What is NOT safe?
A common misconception is that ARC makes everything thread-safe. ARC guarantees the safety of the reference itself, but it does not guarantee the safety of variable assignment.
-
Race Conditions in Pointer Assignment:
If Thread A is pointing variable
xto ObjectO1, while Thread B is pointingxto ObjectO2simultaneously, a crash can occur. Becausexitself is just a pointer, the assignment toxis not atomic.Scenario: Thread A might complete the
releaseofO1, but before it canretainO2, Thread B modifiesx. This results in asymmetric reference counting, leading to a Double Free or a memory corruption crash. -
The Solution: If you need to read and write the same object reference frequently across threads, you must use Locks, Semaphores, or the modern Swift Actor model.
4. The Modern Solution: Actors
To solve the "unsafe pointer assignment" issue described above, Swift introduced the Actor model.
- Isolation: An Actor guarantees that its internal state (including all properties of reference types) can only be accessed by one thread at a time.
- Eliminating Redundant retain/release: Since the compiler knows that the environment inside an Actor is serialized (single-threaded execution), it can optimize away many atomic reference counting operations, thereby boosting performance.
5. Performance vs. Thread Safety Trade-offs
Because retain and release must be atomic, they are slower than non-atomic operations (requiring CPU cache synchronization).
- ARC Elision: The Swift compiler performs ARC Elision. If it can prove that an object will not disappear within a certain scope, it removes redundant atomic instructions.
- Local Objects: For objects used exclusively within a single function, the compiler avoids triggering atomic instructions that involve cross-core synchronization whenever possible.
Summary
| Target | Thread Safe? | Mechanism |
|---|---|---|
| Ref Count Inc/Dec | Yes | Atomic Instructions (LDR/STR Exclusive) |
| Side Table Creation | Yes | Atomic Exchange & Status Bit Checks |
| Pointer Assignment | No | Requires Manual Locks or Actors |
| Object Property Access | No | Requires Manual Locks or Actors |