7
  1. I have got a concurrent queue with dispatch barrier from Raywenderlich post Example

    private let concurrentPhotoQueue = DispatchQueue(label: "com.raywenderlich.GooglyPuff.photoQueue", attributes: .concurrent)

Where write operations is done in

func addPhoto(_ photo: Photo) {
  concurrentPhotoQueue.async(flags: .barrier) { [weak self] in
    // 1
    guard let self = self else {
      return
    }

    // 2
    self.unsafePhotos.append(photo)

    // 3
    DispatchQueue.main.async { [weak self] in
      self?.postContentAddedNotification()
    }
  }
}

While read opeartion is done in

var photos: [Photo] {
  var photosCopy: [Photo]!

  // 1
  concurrentPhotoQueue.sync {

    // 2
    photosCopy = self.unsafePhotos
  }
  return photosCopy
}

As this will resolve Race Condition. Here why only Write operation is done with barrier and Read in Sync. Why is Read not done with barrier and write with sync ?. As with Sync Write, it will wait till it reads like a lock and while barrier Read it will only be read operation.

set(10, forKey: "Number")

print(object(forKey: "Number"))

set(20, forKey: "Number")

print(object(forKey: "Number"))

public func set(_ value: Any?, forKey key: String) {
        concurrentQueue.sync {
            self.dictionary[key] = value
        }
    }
    
    public func object(forKey key: String) -> Any? {
        // returns after concurrentQueue is finished operation
        // beacuse concurrentQueue is run synchronously
        var result: Any?
        
        concurrentQueue.async(flags: .barrier) {
            result = self.dictionary[key]
        }
        
        return result
    }

With the flip behavior, I am getting nil both times, with barrier on Write it is giving 10 & 20 correct

salman siddiqui
  • 882
  • 1
  • 10
  • 28
  • The 2nd example in your revised question is an entirely different issue (and really should be a different question). The issue is not the choice of semaphore vs reader-writer, because any synchronization pattern would be sufficient (though semaphores would be my last choice). The issue is _what_ you synchronize. Specifically, the accessor-level synchronization (which you're doing here) is generally inadequate, you want to synchronize the whole “is there sufficient balance and, if so, deduct the amount from the balance” as a single task, not the individual read and write operations separately. – Rob May 06 '21 at 16:40
  • When one implements synchronization at the `get` and `set` accessor methods, that prevents memory faults and low-level data races, but is generally inadequate to achieve true thread-safety. Often you need a higher level of synchronization. See https://stackoverflow.com/a/58211849/1271826 for an example where even the trivial incrementing of an integer is not-thread-safe, and we need a higher level of abstraction in our synchronization. – Rob May 06 '21 at 16:43

1 Answers1

12

You ask:

Why is Read not done with barrier ... ?.

In this reader-writer pattern, you don’t use barrier with “read” operations because reads are allowed to happen concurrently with respect to other “reads”, without impacting thread-safety. It’s the whole motivating idea behind reader-writer pattern, to allow concurrent reads.

So, you could use barrier with “reads” (it would still be thread-safe), but it would unnecessarily negatively impact performance if multiple “read” requests happened to be called at the same time. If two “read” operations can happen concurrently with respect to each other, why not let them? Don’t use barriers (reducing performance) unless you absolutely need to.

Bottom line, only “writes” need to happen with barrier (ensuring that they’re not done concurrently with respect to any “reads” or “writes”). But no barrier is needed (or desired) for “reads”.

[Why not] ... write with sync?

You could “write” with sync, but, again, why would you? It would only degrade performance. Let’s imagine that you had some reads that were not yet done and you dispatched a “write” with a barrier. The dispatch queue will ensure for us that a “write” dispatched with a barrier won’t happen concurrently with respect to any other “reads” or “writes”, so why should the code that dispatched that “write” sit there and wait for the “write” to finish?

Using sync for writes would only negatively impact performance, and offers no benefit. The question is not “why not write with sync?” but rather “why would you want to write with sync?” And the answer to that latter question is, you don’t want to wait unnecessarily. Sure, you have to wait for “reads”, but not “writes”.

You mention:

With the flip behavior, I am getting nil ...

Yep, so lets consider your hypothetical “read” operation with async:

public func object(forKey key: String) -> Any? {
    var result: Any?

    concurrentQueue.async {
        result = self.dictionary[key]
    }

    return result
}

This effective says “set up a variable called result, dispatch task to retrieve it asynchronously, but don’t wait for the read to finish before returning whatever result currently contained (i.e., nil).”

You can see why reads must happen synchronously, because you obviously can’t return a value before you update the variable!


So, reworking your latter example, you read synchronously without barrier, but write asynchronously with barrier:

public func object(forKey key: String) -> Any? {
    return concurrentQueue.sync {
        self.dictionary[key]
    }
}

public func set(_ value: Any?, forKey key: String) {
    concurrentQueue.async(flags: .barrier) {
        self.dictionary[key] = value
    }
}

Note, because sync method in the “read” operation will return whatever the closure returns, you can simplify the code quite a bit, as shown above.

Or, personally, rather than object(forKey:) and set(_:forKey:), I’d just write my own subscript operator:

public subscript(key: String) -> Any? {
    get {
        concurrentQueue.sync { 
            dictionary[key] 
        } 
    }

    set { 
        concurrentQueue.async(flags: .barrier) {
            self.dictionary[key] = newValue
        }
    }
}

Then you can do things like:

store["Number"] = 10
print(store["Number"])
store["Number"] = 20
print(store["Number"])

Note, if you find this reader-writer pattern too complicated, note that you could just use a serial queue (which is like using a barrier for both “reads” and “writes”). You’d still probably do sync “reads” and async “writes”. That works, too. But in environments with high contention “reads”, it’s just a tad less efficient than the above reader-writer pattern.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • I understood why read is sync because I can't return value before reading so wait is required but for write why its async with barrier any how we are waiting with flag ? – salman siddiqui May 03 '21 at 07:34
  • 1
    Let’s tackle this one step at a time. Why is write async? Like you said, with read, you do it synchronously because you have to wait for the result to be returned. So in write, where we don’t need to return anything, we can therefore no longer need to wait for the dispatched block to finish, which means that we can do it asynchronously, which avoids blocking the caller’s thread during writes. Why do write with barrier? Because we need to make sure that the write does not happen concurrently with anything else on that queue, and that’s what barriers do. But we don’t wait for writes. – Rob May 03 '21 at 10:57
  • 1
    ok got it and for read we are not using "barrier" for concurrency but any how we are using sync which will block the queue like If two “read” operations can happen concurrently with respect to each other any how second need to start after 1 which will impact performance – salman siddiqui May 03 '21 at 11:14
  • Correct, reads can enjoy concurrency (no barrier), but calling thread must wait (sync), but writes can not be concurrent (thus, the barrier), but calling thread doesn’t have to wait (hence async). – Rob May 03 '21 at 16:48
  • edited question for different ways to solve race condition. Is semaphore same as dispatch barrier ? – salman siddiqui May 05 '21 at 16:15
  • @salmansiddiqui E.g. in https://stackoverflow.com/a/58211849/1271826 I benchmarked a bunch of different synchronization mechanisms in Swift. – Rob May 05 '21 at 20:42
  • @salmansiddiqui - So you substantively change the question 2½ years later and then unaccept my answer? Lol. You should have just posted a new question if wanted to ask about other synchronization mechanisms. Or researched it first; there are lots of existing answers (see above) comparing the different synchronization methods. In short, one technically can use semaphores, but it’s the last tool we would reach for. `NSLock` is simple and performant. Unfair locks can be useful where performance is critical, but it’s a little cumbersome and its overkill in most situations. – Rob May 06 '21 at 00:32
  • with above answer I tried to solve different problem you can see in edited question its giving incorrect result that's the reason I unaccepted the answer. – salman siddiqui May 06 '21 at 05:39
  • @Rob out of curiosity, why is the queue needed at all for the read operation. If concurrent reads can happen, does it need to be wrapped in its own dispatch queue? – Dr. Mr. Uncle Oct 21 '21 at 18:21
  • 1
    @Dr.Mr.Uncle - Because reads can only happen concurrently only with respect to other reads. But they cannot be performed concurrently with writes. So concurrent queue allows the reads to happen concurrently, with the exception of writes, which is why we use barrier for writes. – Rob Oct 21 '21 at 18:30
  • Thank you Rob! Very helpful explanation – Dr. Mr. Uncle Oct 21 '21 at 18:35