【问题标题】:How to thread-safe archive a set of custom objects?如何对一组自定义对象进行线程安全存档?
【发布时间】:2018-06-10 14:12:50
【问题描述】:

我有 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

这是正确的方法吗?
我认为这是一个标准问题,应该有一个标准的解决方案。

【问题讨论】:

  • @Rob 感谢您的快速回复。我必须理解他们,并且会回来。
  • @Rob 关于属性级同步:你是完全正确的:为了保持对象整体一致,变异自身或它的任何属性必须在对象级别同步,属性级同步是过时的。关于异步编码:你又是对的:调用者 NSKeyedArchiver 可能假设编码是一个顺序过程,因为所有单独的归档结果都将组合到一个最终数据对象中。
  • @Rob 关于同步模式的简化:有趣!我不知道这个。您的建议有效,但我在 docs 中找不到它。同步不应该有返回值吗?
  • @Rob 再次感谢您的 cmets。也许你可以制定一个答案,以便我可以结束这个问题。

标签: swift multithreading collections thread-safety archiving


【解决方案1】:

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

一些不相关的观察:

  1. encode 方法必须同步执行其任务,而不是异步执行。调用者假定编码在它返回时已经完成。我可以猜到为什么你可能让它异步(例如,它毕竟没有显式返回任何东西),但问题不在于是否返回任何东西,而是更广泛地说是否有任何副作用同步对象。在这种情况下(您正在更新NSCoder 对象),因此您必须在encode 中使用sync

  2. 有几次你使用初始化变量的模式,调用sync 来修改那个局部变量,然后返回那个值。例如

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

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

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

【讨论】:

    【解决方案2】:

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

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

    【讨论】:

      猜你喜欢
      • 2015-08-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-01-18
      • 2011-04-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多