【问题标题】:is reference assignment atomic in Swift 5?Swift 5 中的引用赋值是原子的吗?
【发布时间】:2021-01-21 15:30:44
【问题描述】:

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

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 将是01 ,但我不在乎:无论如何,hasA() 会返回 true,不是吗?

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

【问题讨论】:

  • 实际上用给定的代码——顺便说一下a必须是可选的——你可以用lazy var a = A()替换B中的整个代码,这会懒惰地创建实例(一次),跨度>
  • @vadian 我相信我不能在这里使用lazy:我想知道setA() 是否被显式调用。
  • @vadian lazy 不是线程安全的/也不是原子的。
  • var a: A = nil 这会给你编译错误,你的意思可能是var a: A? = nil 我也同意 Vadian 的观点,即使用惰性将确保那里只存在一个 A 的实例,因为它的原子性
  • “如果 A 是结构而不是类,答案会改变吗?” ... 没有。仍然不是原子的。现在,如果 (1) AB 都是结构体,并且 (2) 你确保每个线程都有自己的副本,在任何地方都使用值语义,那么,是的,这确实解决了问题。

标签: swift thread-safety ios-multithreading


【解决方案1】:

简而言之,不,属性访问器不是原子的。请参阅 WWDC 2016 视频 Concurrent Programming With GCD in Swift 3,其中讨论了该语言中缺乏原子/同步本机。 (这是一个 GCD 演讲,所以当他们随后深入研究同步方法时,他们专注于 GCD 方法,但任何同步方法都可以。)Apple 在自己的代码中使用了各种不同的同步方法。例如。在ThreadSafeArrayStore 他们使用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 的初始化)。这是一个很好的通用解决方案。并且它可以防止逻辑竞争。


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

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

【讨论】:

猜你喜欢
  • 1970-01-01
  • 2019-05-05
  • 2011-01-18
  • 2017-11-01
  • 2011-07-26
  • 2012-05-26
  • 2020-02-03
  • 1970-01-01
  • 2014-08-01
相关资源
最近更新 更多