Home > Mobile >  How to use gcd barrier in iOS?
How to use gcd barrier in iOS?

Time:07-28

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:

  1. The thread that testMutiReadSingleWriteObject is called on (likely the main thread),
  2. The thread that store.queue maintains, and
  3. 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:

  1. 100 getObject calls, then store.object = 1
  2. N getObject calls, then store.object = 1, then (100 - N) getObject calls
  3. store.object = 1, then 100 getObject 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:

  1. Dropping queue, and just accessing store on the thread that the method was called on. This does mean that you cannot call store.getObject asynchronously
  2. Make all calls through queue, whether sync or async. This gives you the opportunity to better control exactly how the store 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.

  • Related