Swift 多线程读变量安全吗?

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

先看测试用例:

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 时会释放。

C++ 复制代码
do {
    swift_retain(tmp)
    let v = tmp
    swift_release(tmp)
}

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

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

1) swift_retain

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

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

C++ 复制代码
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 在使用对象,然后释放它:

C++ 复制代码
// 线程 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」!

相关推荐
初级代码游戏18 小时前
iOS开发 SwiftUI 8:NavigationView 导航
ios·swiftui·swift
虹少侠20 小时前
基于 WebKit 构建 macOS 多浮窗视频播放的技术实践(含完整产品落地)
前端·macos·swift·webkit
开开心心_Every3 天前
文件数量统计工具:支持多层文件夹数量统计
游戏·微信·pdf·excel·语音识别·swift·lisp
QWQ___qwq4 天前
1-s2.0-S0031320324008811-讲解
swiftui
core5126 天前
使用 `ms-swift` 微调 Qwen3-VL-2B 详细指南
lora·微调·swift·qwen·qwen3·vl
core5126 天前
Swift SFT Qwen-VL LoRA 微调指令详解
lora·微调·swift·qwen·vl
Swift社区7 天前
LeetCode 375 - 猜数字大小 II
算法·leetcode·swift
Swift社区8 天前
使用 MetricKit 监控应用性能
ios·swiftui·swift
快手技术8 天前
KwaiDesign:为快手多元业务打造统一、高效的设计与开发体系
swiftui·arkui·weui
Swift社区8 天前
LeetCode 374 猜数字大小 - Swift 题解
算法·leetcode·swift