Using Thread Sanitizer in Tests: Solving Data Races in Concurrency and Multithreading

rozeri dilar
iOS App Mastery
Published in
3 min readJul 14, 2023

Data Races

Data races in concurrent programming occur when multiple threads access the same memory location without proper synchronization, leading to unpredictable program behavior and potential memory corruption. In Swift, we can address data races by utilizing the Thread Sanitizer, an LLVM-based tool that detects such issues at runtime. This article will explore the concept of data races, demonstrate how to use the Thread Sanitizer to identify race conditions, and discuss techniques to synchronize shared resources and mitigate data races.

Understanding Data Races

A data race arises when two or more threads concurrently access the same memory location, with at least one of the accesses being a write operation. This lack of synchronization can result in inconsistent or incorrect program output.

To illustrate this, consider the following example:
Here we are updating the cached images (shared resource):

https://github.com/rozeridilar/Example-App-Data-Races-iOS/blob/main/Sources/Example-Data-Races-In-Image-Loading/ImageCache.swift#L72

Using Thread Sanitizer

The Thread Sanitizer is a powerful tool that can save developers from the arduous task of debugging thread-related issues. However, it is important to note that enabling the Thread Sanitizer incurs significant performance overhead. According to Apple’s documentation, it can lead to a CPU slowdown of 2 to 20 times and increase memory usage by 5 to 10 times. Consequently, it is not advisable to keep the Thread Sanitizer enabled in the main test configuration used for daily development.

Instead, it is more suitable to periodically enable the Thread Sanitizer, particularly during continuous integration (CI) processes. Running the Thread Sanitizer on the CI server ensures thorough checks before merging code into the main branch. However, it’s recommended to assess the performance impact of the Thread Sanitizer on the CI servers and monitor usage to avoid excessive waiting times and increased costs.

Additionally, it’s a good practice to run the Thread Sanitizer locally before triggering a CI build. This allows developers to rapidly identify and address data race issues on their own machines.

To add a thread sanitizer to your test suite:
https://github.com/rozeridilar/Example-App-Data-Races-iOS/commit/c17c5f5ab2cf519a37d4d40c08fa868d94b9c0e4

Synchronizing Memory Access

To mitigate data races and ensure proper synchronization of shared resources, the Dispatch framework APIs in Swift can be leveraged. One approach is to use a DispatchQueue to perform all read and write operations synchronously in a centralized queue. Consider the following example in this commit:

https://github.com/rozeridilar/Example-App-Data-Races-iOS/commit/206a8cb8a54dcdc68b225f07d1a20a088c5ddc29

By utilizing Dispatch queues and applying synchronization techniques, we can effectively prevent data races and ensure consistent and reliable behavior in concurrent code.

Data races pose significant risks to the stability and correctness of concurrent programs. Through the utilization of the Thread Sanitizer, developers can detect and debug data race issues efficiently. It is important to strike a balance between enabling the Thread Sanitizer’s benefits and managing its performance overhead. By synchronizing memory access using tools like Dispatch queues, we can effectively eliminate data races and promote robust and reliable concurrent programming in Swift.

Validate the Behavior of Concurrent Threads via Tests

Furthermore, to ensure the correctness and reliability of concurrent code, it is crucial to incorporate tests specifically designed to validate the behavior of concurrent threads. These tests should cover various scenarios where multiple threads interact with shared resources. By simulating concurrent execution paths and verifying the expected outcomes, we can detect and address potential data race conditions in our codebase. Including these tests as part of our comprehensive unit test suite enhances the robustness of our concurrent algorithms and provides an additional layer of confidence in the stability of our software.
Here is an example: https://github.com/rozeridilar/Example-App-Data-Races-iOS/blob/main/Tests/Example-Data-Races-In-Image-LoadingTests/ImageCacheTests.swift#L8

References

--

--