如何线程安全地归档一组自定义对象?

How to thread-safe archive a set of custom objects?

我有类型为 Set<CostumObject> 的实例,我想使用 NSKeyedArchiver 对其进行存档。

假设 customObject1: CostumObjectcustomObject2: CostumObject 在某处实例化。

如果我使用以下语句:

let setOfCostomObjects: Set<CostumObject> = [customObject1, customObject2]
let data = NSKeyedArchiver.archivedData(withRootObject: setOfCostomObjects)

NSKeyedArchiver 按顺序归档两个自定义对象,其中它们的属性递归归档。

这不是线程安全的,因为另一个线程可以在归档期间改变自定义对象及其属性。

我想我可以线程安全地归档自定义对象的每个 属性,这样允许并发获取但只允许一个集合,方法是使用一个带有集合屏障的并发队列,例如:

private let concurrentPropertyAccessQueue = DispatchQueue(label: "concurrentPropertyAccessQueue", attributes: .concurrent)
…
private var safeProperty = CostumProperty.init()
public private(set) var property: CostumProperty {
  get {
    var result = CostumProperty.init()
    concurrentPropertyAccessQueue.sync { result = safeProperty } // sync, because result is returned
    return result
  } // get
  set { concurrentPropertyAccessQueue.async(flags: .barrier) { safeProperty = newValue } // executes locked after all gets
  } // set
}  
…
public func threadSafeArchiveOfProperty() -> Data {
    var data = Data.init()
    concurrentPropertyAccessQueue.sync {  // sync, because result is returned
      data = NSKeyedArchiver.archivedData(withRootObject: self.safeProperty) 
    }
    return data
}

我想我也可以用类似的方式对整个自定义对象进行线程安全归档:

private let concurrentObjectAccessQueue = DispatchQueue(label: "concurrentObjectAccessQueue", attributes: .concurrent)
…
public func encode(with aCoder: NSCoder) {
    concurrentObjectAccessQueue.async(execute: {
        aCoder.encode(self.property forKey: "property")
        …
    })
}

问题仍然是,如何线程安全地归档自定义对象集。
这将要求在归档期间锁定对集合元素的写访问。

一种方法可能是定义一个全局并发队列:

public let globalConcurrentAccessQueue = DispatchQueue(label: "globalConcurrentAccessQueue", attributes: .concurrent)  

要在归档期间锁定集合及其所有元素,可以编写 Set 类型的扩展,定义 func threadSafeArchiveOfSet() 如上所述。
然后这个函数将覆盖 Set 的 encode(with aCoder: NSCoder),因此 globalConcurrentAccessQueue 被锁定。

这是正确的方法吗?
我觉得这是一个应该有标准解的标准问题

通常,属性 级同步根本不够用。它提供对单个属性的线程安全访问,但不能确保对更广泛的对象的线程安全访问,因为不同属性之间可能存在相互依赖关系。原型示例是具有名字和姓氏属性的 Person 对象。分别对名字和姓氏进行同步更改仍可能导致以内部不一致状态捕获的对象。您经常需要在更高级别同步对象,如果您这样做,它会使 属性 级别的同步变得冗余。

一些不相关的观察:

  1. encode 方法必须同步执行其任务,而不是异步执行。调用者假定编码在 return 秒时完成。我可以猜到你为什么让它异步(例如,毕竟它没有明确 returning 任何东西),但问题不是是否有任何东西被 returned,而是更广泛地说是否在同步对象之外有任何副作用。在这种情况下有(您正在更新 NSCoder 对象),因此您必须在 encode.

  2. 中使用 sync
  3. 有几次您采用初始化变量的模式,调用 sync 修改该局部变量,然后 returning 该值。例如

    func threadSafeArchiveOfProperty() -> Data {
        var data = Data.init()
        concurrentPropertyAccessQueue.sync {  // sync, because result is returned
            data = NSKeyedArchiver.archivedData(withRootObject: self.safeProperty) 
        }
        return data
    }
    

    但是 sync 提供了一个很好的方法来简化它,即如果闭包 return 是一个值,sync 也会 return 它。如果闭包只有一行,你甚至不需要在闭包中显式 return:

    func threadSafeArchiveOfProperty() -> Data {
        return concurrentPropertyAccessQueue.sync {  // sync, because result is returned
            NSKeyedArchiver.archivedData(withRootObject: self.safeProperty) 
        }
    }
    

Basem Emara 描述了here 线程安全数组的解决方案,也可以应用于集合:

他声明了一个 SynchronizedArray 来模拟一个规则数组。其中包含一个私有的并发队列和数组,并暴露了数组的几个属性和方法。
不可变访问是同步和并发完成的,而可变访问是通过屏障异步完成的,即在队列中的所有其他块终止之后。