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」!

相关推荐
大熊猫侯佩14 小时前
WWDC26:把 SwiftUI Toolbar 真正握在手心里
swiftui·swift·apple
大熊猫侯佩19 小时前
WWDC 26 全新 ResultsObserver:终于能在 View 外面盯着 SwiftData 了
swiftui·swift·apple
大熊猫侯佩19 小时前
别再把大模型供在云端了,WWDC26 CoreAI 大模型下凡实战
ai编程·swift·wwdc
00后程序员张20 小时前
有没有更轻量的 iOS 开发环境?快蝎kxapp轻量化路径与构成
ide·vscode·ios·objective-c·个人开发·swift·敏捷流程
sakiko_2 天前
Swift学习笔记39-实战注意事项
前端·笔记·学习·swift
东坡肘子2 天前
Apple Intelligence 已通过审核,即将在中国提供服务 -- 肘子的 Swift 周报 #148
人工智能·swiftui·swift
初级代码游戏3 天前
iOS开发 Swift 速记7:结构体和类
开发语言·ios·swift
初级代码游戏5 天前
iOS开发 Swift 速记5:高级运算符
ios·移动开发·swift
初级代码游戏5 天前
iOS开发 Swift 速记4:函数与闭包
开发语言·ios·swift
ZacJi5 天前
当苹果将系统 API 划归 @MainActor:一次跨 SDK 版本的 Swift 并发踩坑与破局实战
ios·swift