Data Racing in Swift

· 2 min read
Data Racing in Swift
Data racing in Swift and how to avoid them

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.