A data race — two threads touching the same memory, at least one writing, without synchronisation — is undefined behaviour. A mutex makes a critical section run in one thread at a time.
A race, and the fix
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
int main() {
long long counter = 0;
std::mutex m;
{
std::vector<std::jthread> threads;
for (int t = 0; t < 4; ++t)
threads.emplace_back([&] {
for (int i = 0; i < 100'000; ++i) {
std::lock_guard<std::mutex> lock(m); // locks now, unlocks at end of scope
++counter; // without the lock: lost updates
}
});
}
std::cout << counter << '\n';
}400000Lock types
| RAII lock | Use |
|---|---|
std::lock_guard<M> | Simplest: lock for the whole scope |
std::unique_lock<M> | Can unlock/relock, defer, time out, be moved; required by condition variables |
std::scoped_lock<M1, M2…> (C++17) | Lock several mutexes at once without deadlock |
std::shared_lock<std::shared_mutex> | Many readers or one writer |
Deadlock
Thread A locks m1 then waits for m2; thread B locks m2 then waits for m1 — both wait forever. Prevent it by always acquiring locks in the same global order, or by locking them together with std::scoped_lock.
#include <iostream>
#include <mutex>
#include <thread>
struct Account {
int balance;
std::mutex m;
};
void transfer(Account& from, Account& to, int amount) {
std::scoped_lock lock(from.m, to.m); // deadlock-free, whatever order callers use
from.balance -= amount;
to.balance += amount;
}
int main() {
Account a{1000, {}}, b{1000, {}};
{
std::jthread t1([&] { for (int i = 0; i < 1000; ++i) transfer(a, b, 1); });
std::jthread t2([&] { for (int i = 0; i < 1000; ++i) transfer(b, a, 2); });
}
std::cout << a.balance << ' ' << b.balance << ' ' << a.balance + b.balance << '\n';
}2000 0 2000Readers–writer lock
#include <iostream>
#include <map>
#include <mutex>
#include <shared_mutex>
#include <string>
#include <thread>
#include <vector>
class Config {
mutable std::shared_mutex m_;
std::map<std::string, int> values_;
public:
int get(const std::string& k) const {
std::shared_lock lock(m_); // many readers at once
auto it = values_.find(k);
return it == values_.end() ? 0 : it->second;
}
void set(const std::string& k, int v) {
std::unique_lock lock(m_); // exclusive writer
values_[k] = v;
}
};
int main() {
Config cfg;
cfg.set("threads", 4);
std::vector<std::jthread> readers;
std::vector<int> seen(4);
for (int i = 0; i < 4; ++i) readers.emplace_back([&, i] { seen[i] = cfg.get("threads"); });
readers.clear(); // join all
std::cout << seen[0] + seen[1] + seen[2] + seen[3] << '\n';
}16What are the four conditions for deadlock?