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

Posted

技术标签:

【中文标题】Swift 5 中的引用赋值是原子的吗?【英文标题】:is reference assignment atomic in 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) 你确保每个线程都有自己的副本,在任何地方都使用值语义,那么,是的,这确实解决了问题。 【参考方案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
            
        
    

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

【讨论】:

非常感谢!这些例子肯定会在更广泛的背景下有所帮助。我的用例不同。 好的,现在我开始讨论另一个原子性问题:***.com/questions/66224497/…

以上是关于Swift 5 中的引用赋值是原子的吗?的主要内容,如果未能解决你的问题,请参考以下文章

java的原子性操作有哪些

Delphi中的整数读取是原子的吗?

Java中的哪些操作被认为是原子的?

弱指针的reset成员函数是原子的吗?

django 的 bulk_create 是原子的吗?

Linux 上的管道读取是原子的吗(多个写入器,一个读取器)?