Swift 5 中的引用赋值是原子的吗?

Ale*_*ohn 8 thread-safety swift ios-multithreading

在这种情况下我需要某种显式同步吗?

class A { 
  let val: Int; 
  init(_ newVal: Int) { 
    val = newVal
   }
}

public class B {
  var a: A? = nil
  public func setA() { a = A(0) }
  public func hasA() -> Bool { return a != nil }
}
Run Code Online (Sandbox Code Playgroud)

B类中还有另一种方法:

public func resetA() {
  guard hasA() else { return }
  a = A(1)
}
Run Code Online (Sandbox Code Playgroud)

setA()并且resetA()可以从任何线程以任何顺序调用。

我知道可能存在竞争条件,如果同时一个线程调用setA()和另一个线程调用resetA(),则结果无法确定:val要么是0,要么是1,但我不在乎:无论如何,hasA()将返回 true,won'它吗?

如果Astruct而不是class,答案会改变吗?

Rob*_*Rob 2

简而言之,不,属性访问器不是原子的。请参阅 WWDC 2016 视频ConcurrentProgramming With GCD in Swift 3,其中讨论了该语言中原生原子/同步的缺失。(这是一个 GCD 演讲,因此当他们随后深入研究同步方法时,他们会重点关注 GCD 方法,但任何同步方法都可以。)Apple 在自己的代码中使用了各种不同的同步方法。例如,ThreadSafeArrayStore他们使用他们使用NSLock)。

\n
\n

如果与锁同步,我可能会建议如下扩展:

\n
extension NSLocking {\n    func synchronized<T>(block: () throws -> T) rethrows -> T {\n        lock()\n        defer { unlock() }\n        return try block()\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

Apple 在自己的代码中使用了这种模式,尽管他们碰巧称其withLocksynchronized. 但模式是一样的。

\n

然后你可以这样做:

\n
public class B {\n    private var lock = NSLock()\n    private var a: A?             // make this private to prevent unsynchronized direct access to this property\n\n    public func setA() {\n        lock.synchronized {\n            a = A(0)\n        }\n    }\n\n    public func hasA() -> Bool {\n        lock.synchronized {\n            a != nil\n        }\n    }\n\n    public func resetA() {\n        lock.synchronized {\n            guard a != nil else { return }\n            a = A(1)\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

也许

\n
public class B {\n    private var lock = NSLock()\n    private var _a: A?\n\n    public var a: A? {\n        get { lock.synchronized { _a } }\n        set { lock.synchronized { _a = newValue } }\n    }\n\n    public var hasA: Bool {\n        lock.synchronized { _a != nil }\n    }\n\n    public func resetA() {\n        lock.synchronized {\n            guard _a != nil else { return }\n            _a = A(1)\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n
\n

我承认暴露 时有些不安hasA,因为它实际上邀请应用程序开发人员编写如下内容:

\n
if !b.hasA {\n    b.a = ...\n}\n
Run Code Online (Sandbox Code Playgroud)\n

就防止同时访问内存而言,这很好,但如果两个线程同时执行此操作,则它会引入逻辑竞争,其中两个线程都碰巧通过了测试!hasA,并且它们都替换了该值,最后一个获胜。

\n

相反,我可能会编写一个方法来为我们执行此操作:

\n
public class B {\n    private var lock = NSLock() // replacing os_unfair_lock_s()\n    private var _a: A? = nil // fixed, thanks to Rob\n\n    var a: A? {\n        get { lock.synchronized { _a } }\n        set { lock.synchronized { _a = newValue } }\n    }\n\n    public func withA(block: (inout A?) throws -> T) rethrows -> T {\n        try lock.synchronized {\n            try block(&_a)\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

这样你就可以这样做:

\n
public class B {\n    private var lock = NSLock() // replacing os_unfair_lock_s()\n    private var _a: A? = nil // fixed, thanks to Rob\n\n    var a: A? {\n        get { lock.synchronized { _a } }\n        set { lock.synchronized { _a = newValue } }\n    }\n\n    public func withA(block: (inout A?) throws -> T) rethrows -> T {\n        try lock.synchronized {\n            try block(&_a)\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

这是线程安全的,因为我们让调用者将所有逻辑任务(检查是否anil,如果是,则初始化a)全部包装在一个同步步骤中。这是该问题的一个很好的通用解决方案。它可以防止逻辑竞争。

\n
\n

上面的例子太抽象了,很难理解。那么让我们考虑一个实际的例子,A​​pple\xe2\x80\x99s 的变体ThreadSafeArrayStore

\n
b.withA { a in\n    if a == nil {\n        a = ...\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

这里我们有一个同步数组,我们在其中定义一个接口以线程安全的方式与底层数组交互。

\n
\n

或者,如果您想要一个更简单的示例,请考虑使用线程安全对象来跟踪最高的项目是什么。我们不会有hasValue布尔值,而是将其合并到我们的同步updateIfTaller方法中:

\n
public class ThreadSafeArrayStore<Value> {\n    private var underlying: [Value]\n    private let lock = NSLock()\n\n    public init(_ seed: [Value] = []) {\n        underlying = seed\n    }\n\n    public subscript(index: Int) -> Value {\n        get { lock.synchronized { underlying[index] } }\n        set { lock.synchronized { underlying[index] = newValue } }\n    }\n\n    public func get() -> [Value] {\n        lock.synchronized {\n            underlying\n        }\n    }\n\n    public func clear() {\n        lock.synchronized {\n            underlying = []\n        }\n    }\n\n    public func append(_ item: Value) {\n        lock.synchronized {\n            underlying.append(item)\n        }\n    }\n\n    public var count: Int {\n        lock.synchronized {\n            underlying.count\n        }\n    }\n\n    public var isEmpty: Bool {\n        lock.synchronized {\n            underlying.isEmpty\n        }\n    }\n\n    public func map<NewValue>(_ transform: (Value) throws -> NewValue) rethrows -> [NewValue] {\n        try lock.synchronized {\n            try underlying.map(transform)\n        }\n    }\n\n    public func compactMap<NewValue>(_ transform: (Value) throws -> NewValue?) rethrows -> [NewValue] {\n        try lock.synchronized {\n            try underlying.compactMap(transform)\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

仅举几个例子。希望它能说明这个想法。

\n