8-4.【内存管理机制】ARC 如何处理线程安全问题?

33 阅读5分钟

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 完成了对 O1release,但在 retain O2 之前,线程 B 修改了 x。这会导致引用计数的非对称操作,最终引发 Double Free(双重释放)或内存崩溃。

  • 解决办法:如果你需要在多线程间频繁读写同一个对象引用,必须使用锁(Lock)、信号量(Semaphore)或 Swift 现代并发中的 Actor


4. 现代并发的终极方案:Actor

为了解决上述“指针赋值不安全”的问题,Swift 引入了 Actor 模型。

  • 隔离性:Actor 保证了其内部状态(包括所有引用类型的属性)在同一时间只能被一个线程访问。
  • 消除冗余 retain/release:在 Actor 内部处理逻辑时,由于编译器知道环境是单线程执行的(串行化),它可以优化掉大量的原子性引用计数操作,从而提升性能。

5. 性能与线程安全的权衡

因为 retainrelease 必须是原子的,所以它们比非原子操作要慢(需要同步 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) and stxr (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 x to Object O1, while Thread B is pointing x to Object O2 simultaneously, a crash can occur. Because x itself is just a pointer, the assignment to x is not atomic.

    Scenario: Thread A might complete the release of O1, but before it can retain O2, Thread B modifies x. 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

TargetThread Safe?Mechanism
Ref Count Inc/DecYesAtomic Instructions (LDR/STR Exclusive)
Side Table CreationYesAtomic Exchange & Status Bit Checks
Pointer AssignmentNoRequires Manual Locks or Actors
Object Property AccessNoRequires Manual Locks or Actors