Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to handle Race Condition Read/Write Problem in Swift?

  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

like image 217
salman siddiqui Avatar asked Aug 30 '18 12:08

salman siddiqui


People also ask

How do you solve a race condition problem?

To avoid race conditions, any operation on a shared resource – that is, on a resource that can be shared between threads – must be executed atomically. One way to achieve atomicity is by using critical sections — mutually exclusive parts of the program.

What is race condition in Swift?

A race condition occurs when the timing or order of events affects the correctness of a piece of code. Data Race. A data race occurs when one thread accesses a mutable object while another thread is writing to it.

How can we avoid race condition in OS?

To prevent the race conditions from occurring, you can lock shared variables, so that only one thread at a time has access to the shared variable.

What is race condition and how it can be eliminated?

It can be eliminated by using no more than two levels of gating. An essential race condition occurs when an input has two transitions in less than the total feedback propagation time. Sometimes they are cured using inductive delay line elements to effectively increase the time duration of an input signal.


1 Answers

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.

like image 70
Rob Avatar answered Nov 14 '22 22:11

Rob