Data Race with Race Condition? Difference and Solution for both

2 minute read

What are they? Matters? 🀔🆘

Data Race and Race Condition are DIFFERENT

  • Data-Race → MORE than 1 thread access to shared-resource (can be a variable or object) and AT LEAST 1 thread tries to modify that resource
  • Race-Condition → Order of threads’ execution leads to wrong program behavior
  • Many race conditions are due to data races, and many data races lead to race conditions. Can have race conditions without data races and data races without race conditions.

Example:

1@discardableResult
2func transfer(amount: Int, from source: BankAccount, to destination: BankAccount) -> Bool {
3   if source.balance >= amount {
4       destination.balance += amount
5       source.balance -= amount
6       return true
7   }
8   return false
9}

🔵 LET’S RUN ON ONE-SINGLE THREAD

1let accountA = BankAccount(balance: 100)
2let accountB = BankAccount(balance: 100)
3
4transfer(amount: 50, from: accountA, to: accountB)
5
6print(accountA.balance) // 5⃣0⃣
7print(accountB.balance) // 1⃣5⃣0⃣

It makes sense! ✅

🔎 LET’S RUN ON MULTIPLE THREADS

 1let accountA = BankAccount(balance: 1000)
 2let accountB = BankAccount(balance: 2000)
 3
 4...
 5@IBAction private func transferButtonTapped() {
 6    let group = DispatchGroup()
 7    for _ in 1...5 {
 8        DispatchQueue.global().async(group: group) {
 9            self.transfer(amount: 50, from: self.accountA, to: self.accountB)
10        }
11    }
12    
13    group.notify(queue: DispatchQueue.main) {
14        self.labelAccountA.text = "\(self.accountA.balance)"
15        self.labelAccountB.text = "\(self.accountB.balance)"
16    }
17}
18...

We will get a warning ⛔ in transfer(amount:from:to) with the help of Thread Sanitizer as Data race in *.ViewController.transfer(amount: Swift.Int, from: BankAccount, to: BankAccount) -> Swift.Bool at 0x7b080002f8e0

  • ONLY mention about the ORDER of execution, which thread goes first will lead to different outcomes
  • IN REALITY, while a thread is checking the condition and execute destination.balance += amount (we suppose), other threads is also going through these steps and unexpectedly modify the same data → the outcome is unpredictable and can even cause crash 😫

Solutions

GCD to solve Data-Race

Using a serial queue, guarantee only one thread access balances Data-race is now addressed! ⚠ Race-condition is still able to happen, cannot manage the order of execution of threads. But it won’t break the app, which is acceptable!

 1let serialQueue = DispatchQueue(label: "serial.queue")
 2
 3@discardableResult
 4func transfer(amount: Int, from source: BankAccount, to destination: BankAccount) -> Bool {
 5    serialQueue.sync {
 6        if source.balance >= amount {
 7            destination.balance += amount
 8            source.balance -= amount
 9            return
10        }
11        
12        return false
13    }
14}

📚 Learned from:

  1. https://www.swiftbysundell.com/articles/avoiding-race-conditions-in-swift/
  2. https://www.avanderlee.com/swift/race-condition-vs-data-race/
  3. https://www.avanderlee.com/swift/exc-bad-access-crash/
  4. https://www.avanderlee.com/swift/thread-sanitizer-data-races/
  5. https://swiftsenpai.com/swift/actor-prevent-data-race/
  6. https://stackoverflow.com/a/18049303
  7. https://blog.regehr.org/archives/490