swiftmultithreadingcollectionsthread-safetyarchiving

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


I have instances of type Set<CostumObject> that I want to archive using a NSKeyedArchiver.

Assume customObject1: CostumObject and customObject2: CostumObject are instantiated somewhere.

If I use the following statements:

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

NSKeyedArchiver archives sequentially both custom objects where their properties are archived recursively.

This is not thread-safe, since another thread can mutate both custom objects and their properties during archiving.

I think I can thread-safe archive each property of the custom object, so that concurrent gets are allowed but only a single set, by using a concurrent queue with a barrier for set like:

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
}

I think I can also thread-safe archive the whole custom object in a similar way:

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

The problem still is, how to thread-safe archive the set of custom objects.
This would require that write accesses to the elements of the set are locked out during archiving.

One way to do so is probably to define a global concurrent queue:

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

To lock the set and all its elements during archiving, one could probably write an extension to the Set type that defines a func threadSafeArchiveOfSet() as above.
This function would then override the Set’s encode(with aCoder: NSCoder), so that the globalConcurrentAccessQueue is locked.

Is this the right way to go?
I think this is a standard problem that should have a standard solution.


Solution

  • Often, property-level synchronization is simply inadequate. It provides thread-safe access to the individual properties, but it does not ensure thread-safe access to the broader object where there might be interdependencies between different properties. The prototypical example is a Person object with first and last name properties. Synchronization changes to the first and last name separately can still end up with the object being captured in an internally inconsistent state. You often need to synchronize the object at a higher level, and if you do that, it renders the property-level synchronization redundent.

    A few unrelated observations:

    1. The encode method must perform its task synchronously, not asychronously. The caller assumes the encoding is complete by the time it returns. I can guess why you may have made it asynchronous (e.g. it isn't explicitly returning anything, after all), but the question isn't whether anything is returned, but rather more broadly whether there are any side-effects outside of the the synchronized object. In this case there are (you're updating the NSCoder object), so you must use sync in encode.

    2. A couple of times you employ a pattern of initializing a variable, calling sync to modify that local variable, and then returning that value. E.g.

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

      But sync offers a nice way to simplify this, namely if the closure returns a value, sync will return it, too. And if the closure has only one line, you don't even need explicit return in the closure:

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