Swift:存储的引用类型的线程安全初始化属性

Swift: thread-safe initialization of the reference type stored property

假设我们有一个 class 属性,它也是一个 class:

class A { }

class B {
   public var a: A // we want to init it in thread-safe manner
}

// Because 2 threads may be doing this
let b = B()
DispatchQueue.concurrentPerform(iterations: 3) { _ in
    let x = b.a // expecting all threads to get the same instance of the property (i.e. same physical address in memory)
}

我知道 lazy 初始化不是线程安全的,但其他选项呢?

例如:

1.内联初始化 属性 与 init 之间有区别吗?

即是:

class B {
   public var a: A = A()
}

更好或更差
class B {
   public var a: A

   init() {
       a = A()
}

从线程安全的角度来看?

2。 属性的init本身是线程安全的吗?

或者我们是否需要将其包装在某种实例中 getter(Java 样式),例如

extension A {
    static var newInstance -> A {
        return A()
    }
}
class B {
    public var a: A = A.newInstance
}  

(Java 理由是:在这种情况下,直到实例创建后才返回指针,而在直接初始化的情况下,指针在初始化仍为 运行 时分配。

注意:是的,我做了一些测试。而且我没有遇到“崩溃”/没有看到上述任何一种方法的任何问题或差异 - 似乎上述所有三个选项都是相等的(从经验测试的角度来看)。但也许某处有更明确的答案。

Is there a difference between initializing property inline vs. in the init?

不,在 init 内部分配给 属性 或在 init 外部为其提供默认值之间没有任何有意义的区别。默认属性在调用初始化程序之前立即分配,因此

class X {
    var y = Y()
    var z: Z

    init(z: Z) {
        self.z = z
    }
}

在概念上等同于

class X {
    var y: Y
    var z: Z

    func _assignDefaultValues() {
        y = Y()
    }

    init(z: Z) {
        _assignDefaultValues()
        self.z = z
    }
}

相当于

class X {
    var y: Y
    var z: Z

    init(z: Z) {
        y = Y()
        self.z = z
    }
}

换句话说,到 init(...) 结束时,所有存储的属性都必须完全初始化,并且使用默认值或显式初始化它们之间没有区别。


Is the init of the property itself thread-safe?

分开来看,我认为这个问题有两个组成部分:

  1. “到 init() returns 时,b.a 是否保证分配给?”,并且
  2. “如果是这样,是否保证以其他线程读取该值的方式完成分配将保证读取与分配的值匹配的值,并且与其他线程看到的相匹配?”,即,读取数值不撕裂?

(1) 的答案是Swift language guide 非常详细地介绍了细节,但有一点要特别说明:

Classes and structures must set all of their stored properties to an appropriate initial value by the time an instance of that class or structure is created. Stored properties can’t be left in an indeterminate state.

这意味着当您能够从

中读取 b
let b = B()

b.a 必须 已分配一个有效的 A 值。

(2) 的答案有点微妙。通常,Swift not 保证 thread-safe 或默认情况下的原子行为,并且没有我能找到的文档或 [=119 中的引用=] 源代码表明 Swift 在初始化期间对 atomic 分配给 Swift 属性做出任何承诺。虽然不可能证明是否定的,但我认为相对安全地说 Swift 不 保证 在没有显式同步的情况下跨线程获得一致的行为。

但是保证的是在b的生命周期内,它在内存中有一个稳定的地址,到那时,b.a 也会有一个稳定的地址。您的原始代码片段在这种 特定 情况下似乎有效的至少部分原因是

  1. 所有线程都从内存中的同一地址读取,
  2. 在 Swift 支持的许多(大多数?)平台上,word-size(32 位平台上为 32 位;64 位平台上为 64 位)读写是原子的,而不是容易撕裂(在将另一部分写入变量之前只从变量中读取值的一部分)——并且 Swift 中的指针是字大小的。这确实保证读取和写入将如您预期的那样跨线程同步,但您不会以这种方式获得无效地址。但是,
  3. 您的代码会在 b.a 之前创建并分配 其他线程,这意味着对 b.a 的分配更有可能“在他们从该内存中读取之前经历“

如果您要在 b.a 产生 concurrentPerform(iterations:) 之后开始分配给 b.a ,那么所有的赌注都将取消,因为您的读取和读取不同步以意想不到的方式交错写入。

总的来说:

  1. 创建 read-only 数据并将其传递给多个线程是不安全的,但 通常 在实践中会按预期工作(但不应依赖! ),
  2. 创建 read-write 数据并传递对多个线程的引用是不安全的,并发突变也 不会 按预期工作,并且
  3. 如果您需要保证来保证变量的安全原子处理和同步,建议您使用同步机制,如锁或原子(例如来自官方swift-atomics)包裹

同样,如果有疑问,建议您 运行 您的代码通过 sanitizer tools offered by LLVM through Xcode — 具体来说,Address Sanitizer 可以捕获任何 memory-related 问题,在这种情况下, Thread Sanitizer 也有助于捕获同步问题和竞争条件。虽然并不完美,但这些工具可以帮助您确信您的代码是正确的。