Data races, mutexes and lock_guard
Two threads increment one counter, 100,000 times each. The expected total is 200000:
#include <iostream>
#include <thread>
int main() {
int counter = 0;
auto count_up = [&counter]() {
for (int i = 0; i < 100000; ++i) {
++counter; // ✗
}
};
std::thread worker(count_up);
count_up();
worker.join();
std::cout << counter << '\n';
}The program builds without warnings, but the total is not guaranteed. A run may print less than 200000, a different number each time, but the standard does not promise even that.
Data races
++counter is not one action but three: read the value, add one, write the result. If both threads read 5, both write 6, and one increment is lost. A data race is unsynchronized access by two threads to one variable, where at least one of them writes. A data race is undefined behavior for the whole program, not only a wrong number.
Mutexes and lock_guard
A mutex lets one thread at a time reach the data. std::mutex from the <mutex> header has the methods lock() and unlock(). While one thread holds the mutex locked, lock() in another thread waits. Calling them by hand is risky: an early return skips unlock(), and the other threads wait forever.
That is why a mutex is locked through std::lock_guard, which is RAII: the constructor calls lock(), and the destructor calls unlock().
std::mutex m;
auto count_up = [&counter, &m]() {
for (int i = 0; i < 100000; ++i) {
std::lock_guard<std::mutex> guard(m);
++counter;
}
};The mutex is released when the scope of guard ends: here, at the end of each iteration. On any other exit from the scope, such as a return, the destructor releases the mutex too. Now the program always prints 200000.
Common mistake
Two threads lock two mutexes in opposite orders:
std::mutex a;
std::mutex b;
void left() {
std::lock_guard<std::mutex> ga(a);
std::lock_guard<std::mutex> gb(b);
}
void right() {
std::lock_guard<std::mutex> gb(b);
std::lock_guard<std::mutex> ga(a); // ✗
}If one thread runs left while another runs right, the program can hang forever. The first thread locks a and waits for b, while the second locks b and waits for a. Such mutual waiting is called a deadlock. It does not happen on every run, which makes it hard to find.
std::scoped_lock locks several mutexes at once and without a deadlock, whatever order they are passed in: std::scoped_lock lock(a, b);. It releases them at the end of its scope, too.