I want to use gcd barrier implement a safe store object. But it not work correctly. The setter sometime is more early than the getter. What's wrong with it? https://gist.github.com/Terriermon/02c446d1238ad6ec1edb08b607b1bf05
class MutiReadSingleWriteObject<T> {
let queue = DispatchQueue(label: "com.readwrite.concurrency", attributes: .concurrent)
var _object:T?
var object: T? {
@available(*, unavailable)
get {
fatalError("You cannot read from this object.")
}
set {
queue.async(flags: .barrier) {
self._object = newValue
}
}
}
func getObject(_ closure: @escaping (T?) -> Void) {
queue.async {
closure(self._object)
}
}
}
func testMutiReadSingleWriteObject() {
let store = MutiReadSingleWriteObject<Int>()
let queue = DispatchQueue(label: "com.come.concurrency", attributes: .concurrent)
for i in 0...100 {
queue.async {
store.getObject { obj in
print("\(i) -- \(String(describing: obj))")
}
}
}
print("pre --- ")
store.object = 1
print("after ---")
store.getObject { obj in
print("finish result -- \(String(describing: obj))")
}
}
Whenever you create a DispatchQueue
, whether serial or concurrent, it spawns its own thread that it uses to schedule and run work items on. This means that whenever you instantiate a MutiReadSingleWriteObject<T>
object, its queue will have a dedicated thread for synchronizing your setter and getObject
method.
However: this also means that in your testMutiReadSingleWriteObject
method, the queue
that you use to execute the 100 getObject
calls in a loop has its own thread too. This means that the method has 3 separate threads to coordinate between:
testMutiReadSingleWriteObject
is called on (likely the main thread),store.queue
maintains, andqueue
maintainsThese threads run their work in parallel, and this means that an async
dispatch call like
queue.async {
store.getObject { ... }
}
will enqueue a work item to run on queue
's thread at some point, and keep executing code on the current thread.
This means that by the time you get to running store.object = 1
, you are guaranteed to have scheduled 100 work items on queue
, but crucially, how and when those work items actually start executing are up to the queue, the CPU scheduler, and other environmental factors. While somewhat rare, this does mean that there's a chance that none of those tasks have gotten to run before the assignment of store.object = 1
, which means that by the time they do happen, they'll see a value of 1
stored in the object.
In terms of ordering, you might see a combination of:
getObject
calls, then store.object = 1
getObject
calls, then store.object = 1
, then (100 - N) getObject
callsstore.object = 1
, then 100 getObject
callsCase (2) can actually prove the behavior you're looking to confirm: all of the calls before store.object = 1
should return nil
, and all of the ones after should return 1
. If you have a getObject
call after the setter that returns nil
, you'd know you have a problem. But, this is pretty much impossible to control the timing of.
In terms of how to address the timing issue here: for this method to be meaningful, you'll need to drop one thread to properly coordinate all of your calls to store
, so that all accesses to it are on the same thread.
This can be done by either:
queue
, and just accessing store
on the thread that the method was called on. This does mean that you cannot call store.getObject
asynchronouslyqueue
, whether sync
or async
. This gives you the opportunity to better control exactly how the store
methods are calledEither way, both of these approaches can have different semantics, so it's up to you to decide what you want this method to be testing. Do you want to be guaranteed that all 100 calls will go through before store.object = 1
is reached? If so, you can get rid of queue
entirely, because you don't actually want those getters to be called asynchronously. Or, do you want to try to cause the getters and the setter to overlap in some way? Then stick with queue, but it'll be more difficult to ensure you get meaningful results, because you aren't guaranteed to have stable ordering with the concurrent calls.