A Single Writer Pattern in Swift
A Fun Experiment With Shared Mutable State and Lock-Free "Locking"
I was talking to a co-worker today about different ways to minimize the risks around having a single shared instance in any app that holds global values which can be read and written to from anywhere in the code.
The potential risks around global mutable state are discussed in lots of places, but some of the key problems are:
Race conditions: app behavior will change depending on whether one piece of code alters the shared state before another piece
Thread unsafety: another thread can change values at the same time those values are being modified or acted upon elsewhere in code
Unpredictable: there is no guarantee that the shared state will remain stable or consistent during a period of time when some code needs it to be
There are a number of solutions to mitigate these problems with shared mutable state, like locks, serial dispatch queues for write operations, etc. But locks can be tricky to implement and work with (and if used incorrectly can deadlock you app) and serial dispatch queues only address the second point above (thread safety).
So I thought an interesting experiment would be a way to wrap mutable state (like, for example, a global User object) in an interface that allowed:
Any code to read the state at any time (which is almost always an inherently safe operation)
Only one call site to write to the state at a time. But rather than locking access and forcing other write requests to "wait" (possibly leading to deadlocks), this writing interface would simply not exist for any other code while one piece of code was using it.
This allows interesting strategies, like using if let
bindings to see if the writing interface is even available, and if not either scheduling another attempt for later (using the writeNext
method, see update further down), or simply taking a different action other than writing to mutable state. Or holding a reference to the writing interface in one controller during a priority operation, and knowing that it is guaranteed that no other code will be able to intercede during the lifetime of that important operation, and then releasing the reference when the exclusive access is no longer needed.
These strategies allow for a degree of predictability and safety that usually don't come easily (or sometimes at all) when dealing with global mutable state.
So I created a quick Gist for a generic SingleWriter wrapper type that uses a weak reference and a few lines of code to create an optional-typed writer object that can only be accessed if no other references to it already exist. Otherwise, the writer object returns as nil, an there is no access to modify the contents of the wrapped struct.
Update:
As an additional convenience, I wanted to see if I could also incorporate a way to queue write operations in the event that the writer interface is currently unavailable (nil). As with the rest of the implementation, this is still accomplished without using potentially unsafe locks, and instead takes advantage of Swift's own memory management mechanisms to detect when the writer interface is released and then "checks it out" for each queued write operation. Interestingly, each queued write operation can also hold the reference it is passed to the writer interface for as long as needed, which will prevent subsequent write operations (including those in the queue) from running until the writer interface is no longer in use by the current operation.
Here's what the full code plus example usage looks like in a Swift playground:
class SingleWriter<T> {
private var value:T
private var nextWriters = [(WriterProxy<T>)->()]()
private weak var proxy: WriterProxy<T>?
/// Returns a read-only copy of the wrapped struct
var read: T { return value }
/// Returns a writer interface that allows changes to the wrapped struct. Only one
/// interface instance can exist at a time
var writer: WriterProxy<T>? {
get {
if self.proxy != nil { return nil }
let proxy = WriterProxy<T>(value:value, updateClosure: { self.value = $0 }){
if let next = self.nextWriters.popLast(), let writer = self.writer {
next(writer)
}
}
self.proxy = proxy
return proxy
}
}
/// Init with any struct instance to provide the single writer interface for
init(value:T) {
self.value = value
}
/// Provide a closure to write modifications to the wrapped struct whenever the
/// writer interface is next available
func writeNext(closure:@escaping (WriterProxy<T>)->()) {
if let writer = writer {
closure(writer)
} else {
nextWriters.insert(closure, at: 0)
}
}
}
class WriterProxy<T> {
var write:T {
didSet {
updateClosure(write)
}
}
private let updateClosure:(T)->()
private let completedClosure:()->()
fileprivate init(value:T, updateClosure:@escaping (T)->(), completedClosure:@escaping ()->()) {
self.write = value
self.updateClosure = updateClosure
self.completedClosure = completedClosure
}
deinit {
completedClosure()
}
}
// Example usage:
// A simple struct type
struct User {
var name:String
var id:Int
}
// Wrap an instance of a User with initial values in a SingleWriter instance
let currentUser = SingleWriter(value: User(name: "Default", id: 0))
print(currentUser.read.name) // prints "Default"
// hold a reference to the "writer" interface for this User instance
var writerReference = currentUser.writer
// modify the name via the reference to the "writer" interface
writerReference?.write.name = "Joe"
// try to also modify the name through a different call to the "writer" interface.
// Because writerReference already has a "lock" on the "writer" interface, this
// attempt to get the "writer" interface returns nil and the write operation
// doesn't take effect
currentUser.writer?.write.name = "Sally"
// notice that only the first modification to the name, via the writerReference
// worked
print(currentUser.read.name) // prints "Joe"
// queue up a modification for when the writer interface is next released
currentUser.writeNext{ $0.write.name = "Bobo" }
// because there is still a reference to the "writer" interface being retained, the
// "Bobo" name change hasn't happened yet
print(currentUser.read.name) // prints "Joe"
// release the retained reference to the single "writer" interface
writerReference = nil
// as soon as the reference to the "writer" interface was released, the queued
// name change to "Bobo" was applied
print(currentUser.read.name) // prints "Bobo"
// and now other code can access the single "writer" interface again as well and
// this will work
currentUser.writer?.write.name = "Sally"
currentUser.writer?.write.id = 10
print(currentUser.read) // prints User(name: "Sally", id: 10)
So all told, this pattern allows for a very flexible and effective alternative to locks and dispatch queues that is implemented completely using Swift's automatic reference counting mechanisms for predictable and safe behavior. Consider this approach the next time you need thread-safety and predictability in your shared mutable state, and share your thoughts or feedback in the comments below!
Posted in: architectureiosswift