In this recipe, we will work with the following example code (example.cpp):
#include <chrono>
#include <iostream>
#include <thread>
static const int num_threads = 16;
void increase(int i, int &s) {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "thread " << i << " increases " << s++ << std::endl;
}
int main() {
std::thread t[num_threads];
int s = 0;
// start threads
for (auto i = 0; i < num_threads; i++) {
t[i] = std::thread(increase, i, std::ref(s));
}
// join threads with main thread
for (auto i = 0; i < num_threads; i++) {
t[i].join();
}
std::cout << "final s: " << s << std::endl;
return 0;
}
In this example code, we start 16 threads, and each of these threads calls the increase function. The increase function sleeps for one second, then prints and increments an integer, s. We anticipate that this example code will manifest data races, because all threads read and modify the same address, without any explicit synchronization or coordination. In other words, we expect that the final s, which is printed at the end of the code, may differ from run to run. The code is buggy, and we will try to identify the data race with the help of ThreadSanitizer. Without running ThreadSanitizer, we may not see any problems with the code:
$ ./example
thread thread 0 increases 01 increases 1
thread 9 increases 2
thread 4 increases 3
thread 10 increases 4
thread 2 increases 5
thread 3 increases 6
thread 13 increases 7
thread thread 7 increases 8
thread 14 increases 9
thread 8 increases 10
thread 12 increases 11
thread 15 increases 12
thread 11 increases 13
5 increases 14
thread 6 increases 15
final s: 16