Swift 多线程读变量安全吗?

前文,我们讲了在 Rust 中多线程读 RefCell 变量不安全的例子(见 Rust RefCell 多线程读为什么也 panic 了?),同样的例子,如果在 Swift 中,多线程读变量安全吗?

先看测试用例:

class Object {
    let value: String
    init(value: String) {
        self.value = value
    }

    deinit {
        print("Object deinit")
    }
}

class Demo {
    var object: Object? = Object(value: String("Hello World"))

    func foo() {
        var tmp = object
        object = nil
        (0..<10000).forEach { index in
            DispatchQueue.global().async {
                do {
                    let v = tmp
                }
                usleep(100)
                print(index, tmp)
            }
        }
    }
}

let demo = Demo()
demo.foo()

多次运行后,​没有崩溃​。

当我们读一个变量时,编译器会自动帮我们插入引用计数的逻辑,类似如下,当对象引用计数为 0 时会释放。

do {
    swift_retain(tmp)
    let v = tmp
    swift_release(tmp)
}

按 Rust 中读 RefCell 变量的思路分析看,Swift 在读变量时也会​涉及 retain、release 来写引用计数​,为什么 Swift 中不会崩溃呢?

我们来扒一下 Swift 的源码:https://github.com/swiftlang/swift

1) swift_retain

引用计数 +1,主要代码如下:

在这里插入图片描述

refCounts 表示引用计数,定义如下,可以看出 refCounts 是一个​原子变量​,这也是保证线程安全的关键。

class RefCounts {
  std::atomic<RefCountBits> refCounts;
  ...
}

masked->refCounts.increment(object, 1)对应函数如下:
在这里插入图片描述

有两处关键代码:

第一个红框表示读取当前引用计数,这是一个原子的读取。

第二个红框,表示 CAS(Compare-And-Swap)更新引用计数,这也是一个​原子操作​,逻辑如下:

  • 比较 (Compare)​:看内存中 refCounts 的当前值,是否还等于刚才读到的 oldbits
  • 如果相等,则交换​:相等说明在计算期间,没有其他线程修改过它,则直接将内存中的值更新为 newbits,并返回 true,循环结束
  • 如果不相等,则重置​:不相等说明在计算期间,有其他线程抢先修改了内存,此时会将 oldbits 更新为内存中那个最新的、被其他线程改过的值,并返回 false,继续循环,用新的 oldbits 再算一次

可以看出​ swift_retain 中对引用计数的读写操作都是原子的。

2) swift_release

引用计数 -1,主要代码如下:

在这里插入图片描述

执行 -1 的代码如下:

在这里插入图片描述

和 swift_retain 很类似,包含两个步骤:

第一个红框是原子的读引用计数。

第二个红框是 CAS 原子的写引用计数。

另外,这里还有另一个点需要注意,swift_release CAS 写引用计数时,传的参数是std::memory_order_release

std::memory_order_release​​​ 的作用是避免指令重排​,表示在该指令执行完成之前,在代码里写在该指令前面的所有内存操作,必须全部同步到内存中,绝对不允许重排到该指令之后执行。

举个例子:

假设线程 A 在使用对象,然后释放它:

// 线程 A
myObject.someData = 100 // 1. 写数据
// ... 使用完毕 ...
release(myObject)       // 2. 减少引用计数 (可能降为0)

如果没有 std::memory_order_release,CPU 或编译器可能会进行指令重排,把 1 和 2 的顺序颠倒,也就是说,可能先减少了引用计数,再写入数据。

如果发生这种情况,可能导致对一个已释放的对象进行写操作,导致崩溃(Use-After-Free)。

可以对比看下​ swift_retain 时传入的参数是​​std::memory_order_relaxed​,这是一种性能开销最小、限制最少的内存排序选择,它只保证这个操作本身是原子的,但不保证和其他代码的执行顺序。这是因为 retain 时不会导致对象释放,即使在引用计数写入后执行代码,也不会有影响。

更多内容,欢迎订阅公众号「非专业程序员Ping」!

posted on 2025-11-21 01:13  非专业程序员Ping  阅读(0)  评论(0)    收藏  举报

导航