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))")
}
}
CodePudding user response:
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:
- The thread that
testMutiReadSingleWriteObject
is called on (likely the main thread), - The thread that
store.queue
maintains, and - The thread that
queue
maintains
These 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:
- 100
getObject
calls, thenstore.object = 1
- N
getObject
calls, thenstore.object = 1
, then (100 - N)getObject
calls store.object = 1
, then 100getObject
calls
Case (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:
- Dropping
queue
, and just accessingstore
on the thread that the method was called on. This does mean that you cannot callstore.getObject
asynchronously - Make all calls through
queue
, whethersync
orasync
. This gives you the opportunity to better control exactly how thestore
methods are called
Either 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.