C++: protecting shared data with std::mutex
Run two threads that increment one counter and use a lock to keep every update.
#include <iostream>
#include <mutex>
#include <thread>
int main() {
int counter = 0;
std::mutex m;
auto work = [&]() {
for (int i = 0; i < 100000; ++i) {
std::lock_guard<std::mutex> lock(m);
++counter;
}
};
std::thread a(work);
std::thread b(work);
a.join();
b.join();
std::cout << "counter=" << counter << "\n";
}counter=200000
Explanation
A data race occurs when two threads access the same memory at the same time and at least one writes, with no synchronization. In C++ a data race is undefined behaviour, not just a wrong answer. The statement ++counter is a read, an add, and a write, so concurrent increments can be lost.
A std::mutex provides mutual exclusion. std::lock_guard locks it when constructed and unlocks it when it goes out of scope, even if an exception is thrown, which is the RAII pattern. join waits for a thread to finish. With the lock in place both threads together always produce 200000.
Explore the dry run
#include <iostream>#include <mutex>#include <thread> int main() { int counter = 0; std::mutex m; auto work = [&]() { for (int i = 0; i < 100000; ++i) { std::lock_guard<std::mutex> lock(m); ++counter; } }; std::thread a(work); std::thread b(work); a.join(); b.join(); std::cout << "counter=" << counter << "\n";}Follow the example
Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.
View the complete dry-run table
| Step / line | What happens | State after step | Output so far |
|---|---|---|---|
| One mutex guards the counter. Every thread must use the same one. | — | — | |
| Lock now; the lock is released automatically at the end of the loop body. | only one thread holds the lock | — | |
| Wait for thread a to finish before reading counter. | only one thread holds the lock | — | |
| Both threads did 100000 protected increments. | counter = 200000 | counter=200000 |
Cost and common mistakes
O(n) work; the lock serializes the updates.
- Destroying a joinable std::thread calls std::terminate. Always join or detach.
- Locking inside the innermost loop is slow. Count locally and add the total under the lock once, or use std::atomic.
- On older Linux toolchains the program must be linked with -pthread.
Try it yourself
How could you avoid the mutex for a plain counter?
Reveal answer and reasoning
Use std::atomic<int> and call fetch_add(1), which makes each increment atomic without a lock.