Data Race with Race Condition? Difference and Solution for both
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:
- https://www.swiftbysundell.com/articles/avoiding-race-conditions-in-swift/
- https://www.avanderlee.com/swift/race-condition-vs-data-race/
- https://www.avanderlee.com/swift/exc-bad-access-crash/
- https://www.avanderlee.com/swift/thread-sanitizer-data-races/
- https://swiftsenpai.com/swift/actor-prevent-data-race/
- https://stackoverflow.com/a/18049303
- https://blog.regehr.org/archives/490