是引用赋值在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 }
}

B中还有另一个方法:

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

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

我明白可能有一个竞争条件,如果并发一个线程调用setA()和另一个线程调用resetA(),结果不确定:val将是0,或1,但我不在乎:无论如何,hasA()将返回真,不会吗?

如果A答案会改变吗?是结构体而不是class

?

简而言之,不,属性访问器不是原子的。参见WWDC 2016视频《Swift 3中使用GCD并发编程》,该视频讨论了Swift语言中原子/同步的缺失。(这是一个关于GCD的演讲,所以当他们随后深入到同步方法时,他们关注的是GCD方法,但任何同步方法都是可以的。)苹果在自己的代码中使用了各种不同的同步方法。例如,在ThreadSafeArrayStore中他们使用他们使用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的初始化)都在一个单一的同步步骤中。这是这个问题的一个很好的一般化解。并且它可以防止逻辑竞争。


上面的例子太抽象了,很难理解。让我们考虑一个实际的例子,一个苹果的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
}
}
}
}

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