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

bvn4nwqk  于 2022-11-21  发布在  Swift
关注(0)|答案(1)|浏览(157)

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

class A { }

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

let b = B()

DispatchQueue.concurrentPerform(iterations: 3) { _ in
    // Because 2 or more threads may be doing this:
    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()
}

从线程安全Angular 来看?

2.属性的init本身是否是线程安全的?

还是需要将其 Package 在某种示例getter(Java风格)中,例如

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

(Java理由是:在这种情况下,直到创建了示例才返回指针,而在直接init的情况下,在init仍在运行时分配指针)。
注:是的,我做了一些测试。我没有得到“崩溃”/没有看到任何问题或差异与上述任何方法-似乎所有三个选项是平等的(从经验测试的Angular )。但也许有一个更明确的答案在某处。

ac1kyiln

ac1kyiln1#

初始化内联属性与在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(...)的结尾时,所有存储的属性都必须完全初始化,并且使用默认值或显式初始化它们没有区别。
属性的init本身是线程安全的吗?
撇开这一点不谈,我认为这个问题有两个组成部分:
1.“在init()返回时,b.a是否保证被赋值给?",以及
1.“如果是这样的话,是否保证赋值以这样一种方式完成:阅读该值的其他线程将被保证读取与赋值相匹配的值,并且与其他线程所看到的相匹配?",即,读取该值而不撕裂?
(1)的答案是yesSwift language guide非常详细地介绍了这些细节,但特别指出:
类和结构 * 必须 * 在创建该类或结构的示例之前将其所有存储属性设置为适当的初始值。存储属性不能处于不确定状态。
这意味着当您能够从中读取b

let b = B()

b.a * 必须 * 已指定有效的A值。
答案(2)有点微妙。通常情况下,Swift * 不 * 保证线程安全或默认情况下的原子行为,并且我没有找到任何文档,也没有Swift源代码中的参考资料表明Swift在初始化期间对Swift属性的 * 原子 * 赋值做出任何承诺。尽管不可能证明否定,我认为可以相对安全地说,Swift不 * 保证 * 在没有显式同步的情况下,您可以在线程之间获得一致的行为。

然而,* 可以保证的是 * 在b的生存期内,它在内存中有一个稳定的地址,并且b.a也将有一个稳定的地址。

1.所有线程都从内存中的同一地址阅读,
1.在Swift支持的许多(大多数?)平台上,字长(在32位平台上为32位;在64位平台上为64位)读写是原子的,不容易被撕裂(只阅读变量值的一部分,然后再写入另一部分)--Swift中的指针是字大小的。这 * 不 * 保证读写会像你期望的那样在线程间同步,但是你不会得到无效的地址。但是,
1.您的代码 * 在 * 其他线程产生之前 * 创建并赋值b.a,这意味着对b.a的赋值更有可能在它们从该内存读取之前“通过
如果在生成concurrentPerform(iterations:) * 之后 * 开始对b.a赋值,那么所有的赌注都将落空,因为您将以意想不到的方式进行不同步的读写交错。
一般而言:
1.创建只读数据并将其传递给多个线程是不安全的,但在实践中 * 通常 * 会按预期工作(但不应依赖!),
1.创建读写数据并传递对多个线程的引用是不安全的,并发变异也将 * 不 * 按预期工作,并且
1.如果您需要 * 保证 * 变量和同步的安全原子处理,建议您使用同步机制,如锁或原子(例如来自官方的swift-atomics)包
如果有疑问,建议您通过sanitizer tools offered by LLVM through Xcode运行代码,特别是通过Address Sanitizer来捕获任何与内存相关的问题,在这种情况下,也可以通过Thread Sanitizer来帮助捕获同步问题和竞争条件。虽然这些工具并不完美,但它们有助于给予您的代码是正确的。

相关问题