68 Intermediate The Hundred
The Guard That Guards Nothing
the shortest critical section in C++
#include <iostream>
#include <mutex>
#include <thread>
std::mutex m;
int counter = 0;
void work() {
for (int i = 0; i < 100000; ++i) {
std::lock_guard<std::mutex>{m};
++counter;
}
}
int main() {
std::thread t1(work);
std::thread t2(work);
t1.join();
t2.join();
std::cout << "counter = " << counter << '\n';
}
Run it. What does it print?
Answer
Rarely 200000. Typical output (GCC on x86-64): counter = 193256 — a different
shortfall every run.
Why¶
A lock guard only guards while it's alive, and this one has no name — it's a
temporary, constructed and destroyed within the same statement. The mutex is locked
and unlocked at the semicolon, so every ++counter runs unprotected: a data race, which
is undefined behavior. Lost updates are the typical symptom; with less contention it
may even appear to work. The vexing cousin std::unique_lock<std::mutex>(m); is sneakier
still: with parentheses it's a declaration of a default-constructed unique_lock
named m — shadowing the real mutex and holding no mutex at all. It only compiles
because unique_lock has a default constructor; lock_guard spelled that way at least
fails with "no matching function for call to ... lock_guard()".
GCC's -Wall flags the parenthesized form (unnecessary parentheses in declaration
of 'm', from -Wparentheses) but says nothing about the unnamed temporary.
The fix¶
Name the guard — a named object lives to the end of its scope:
Takeaway: a lock guard is only as long-lived as its name — no name, no scope, no lock.
Try it: g++ -std=c++17 -pthread main.cpp -o demo && ./demo