Swift 5 中的引用分配是原子的吗?

is reference assignment atomic in Swift 5?

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

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 }
}

class中还有一个方法B:

public func resetA() {
  guard hasA() else { return }
  a = A(1)
}

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

我知道可能存在竞争条件,如果一个线程同时调用 setA() 而另一个线程调用 resetA(),结果不确定:val 要么是01,但我不在乎:无论如何,hasA() 将 return 为真,不是吗?

如果 Astruct 而不是 class[=35=,答案会改变吗]?

简而言之,不,属性 访问器不是原子的。请参阅 WWDC 2016 视频 Concurrent Programming With GCD in Swift 3, which talks about the absence of atomic/synchronization native in the language. (This is a GCD talk, so when they subsequently dive into synchronization methods, they focus on GCD methods, but any synchronization method is fine.) Apple uses a variety of different synchronization methods in their own code. E.g. in ThreadSafeArrayStore they use they use NSLock)。


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

extension NSLocking {
    func synchronized<T>(block: () throws -> T) rethrows -> T {
        lock()
        defer { unlock() }
        return try block()
    }
}

Apple 在他们自己的代码中使用了这种模式,尽管他们恰好将其称为 withLock 而不是 synchronized。但是模式是一样的。

那么你可以这样做:

public class B {
    private var lock = NSLock()
    private var a: A?             // make this private to prevent unsynchronized direct access to this property

    public func setA() {
        lock.synchronized {
            a = A(0)
        }
    }

    public func hasA() -> Bool {
        lock.synchronized {
            a != nil
        }
    }

    public func resetA() {
        lock.synchronized {
            guard a != nil else { return }
            a = A(1)
        }
    }
}

或者

public class B {
    private var lock = NSLock()
    private var _a: A?

    public var a: A? {
        get { lock.synchronized { _a } }
        set { lock.synchronized { _a = newValue } }
    }

    public var hasA: Bool {
        lock.synchronized { _a != nil }
    }

    public func resetA() {
        lock.synchronized {
            guard _a != nil else { return }
            _a = A(1)
        }
    }
}

我承认在公开 hasA 时有些不安,因为它实际上邀请应用程序开发人员这样写:

if !b.hasA {
    b.a = ...
}

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

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

public class B {
    private var lock = NSLock() // replacing os_unfair_lock_s()
    private var _a: A? = nil // fixed, thanks to Rob

    var a: A? {
        get { lock.synchronized { _a } }
        set { lock.synchronized { _a = newValue } }
    }

    public func withA(block: (inout A?) throws -> T) rethrows -> T {
        try lock.synchronized {
            try block(&_a)
        }
    }
}

你可以这样做:

b.withA { a in
    if a == nil {
        a = ...
    }
}

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


现在上面的例子太抽象了,很难理解。那么让我们考虑一个实际的例子,A​​pple ThreadSafeArrayStore:

的变体
public class ThreadSafeArrayStore<Value> {
    private var underlying: [Value]
    private let lock = NSLock()

    public init(_ seed: [Value] = []) {
        underlying = seed
    }

    public subscript(index: Int) -> Value {
        get { lock.synchronized { underlying[index] } }
        set { lock.synchronized { underlying[index] = newValue } }
    }

    public func get() -> [Value] {
        lock.synchronized {
            underlying
        }
    }

    public func clear() {
        lock.synchronized {
            underlying = []
        }
    }

    public func append(_ item: Value) {
        lock.synchronized {
            underlying.append(item)
        }
    }

    public var count: Int {
        lock.synchronized {
            underlying.count
        }
    }

    public var isEmpty: Bool {
        lock.synchronized {
            underlying.isEmpty
        }
    }

    public func map<NewValue>(_ transform: (Value) throws -> NewValue) rethrows -> [NewValue] {
        try lock.synchronized {
            try underlying.map(transform)
        }
    }

    public func compactMap<NewValue>(_ transform: (Value) throws -> NewValue?) rethrows -> [NewValue] {
        try lock.synchronized {
            try underlying.compactMap(transform)
        }
    }
}

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


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

public class Tallest {
    private var _height: Float?
    private let lock = NSLock()

    var height: Float? {
        lock.synchronized { _height }
    }

    func updateIfTaller(_ candidate: Float) {
        lock.synchronized {
            guard let tallest = _height else {
                _height = candidate
                return
            }

            if candidate > tallest {
                _height = candidate
            }
        }
    }
}

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