A data race in Swift occurs when two or more threads access the same memory location simultaneously and at least one of them is writing to it, without proper synchronization. This can lead to unexpected behavior or crashes in your program. In Swift, there are two common scenarios on data racing:
Variable accessed by multiple threads
Here's an typical example of a data race in Swift:
class Counter {
var count = 0
func increment() {
count += 1
}
}
let counter = Counter()
// Start two concurrent threads
DispatchQueue.concurrentPerform(iterations: 2) { index in
for _ in 1...10000 {
counter.increment()
}
}
print(counter.count)
// This might print a value different than 20000, i.e. 19994, 19999
In this example, DispatchQueue.concurrentPerform
means there are 2 concurrent threads are incrementing a shared Counter
object's count
property. Without proper synchronization, it's possible that the count
property will be read and written to simultaneously by the two threads, leading to unexpected results.
One way to avoid data race is by using synchronization primitives such as DispatchQueue.sync
and NSLock
. For example:
class Counter {
private(set) var count = 0
private let queue = DispatchQueue(label: "com.example.counter")
func increment() {
queue.sync {
count += 1
}
}
}
It's important to be aware of data races and to use appropriate synchronization techniques to avoid them, as they can lead to unexpected behavior or crashes in your program.
inout parameter and & operator in Method
In Swift, it's also important to be aware of the inout
parameter and the &
operator, because they can also lead to data race. If you use inout
or &
operator on a variable that is shared between multiple threads, the access to that variable will not be thread safe.
Here is an example of data racing:
var counter = 0
DispatchQueue.concurrentPerform(iterations: 10000) { _ in
incrementCounter(&counter)
}
func incrementCounter(_ counter: inout Int) {
counter += 1
}
print("output: \(counter)")
// like the example above, it might be 9623, 9998 and so on.
To fix above, we can use the NSLock
:
var counter = 0
let lock = NSLock()
DispatchQueue.concurrentPerform(iterations: 10000) { index in
incrementCounter(&counter)
}
func incrementCounter(_ counter: inout Int) {
lock.lock()
counter += 1
lock.unlock()
}
print("output \(counter)") // always output 10000
Summary
It's important to be aware of data races and to use appropriate synchronization techniques to avoid them, as they can lead to unexpected behavior or crashes in your program.