Exercises — Concurrency — std - thread, std - mutex, std - lock_guard, std - unique_lock
Before we start, one picture fixes the vocabulary we lean on the whole page — what a lock actually does to overlapping threads in time.

Read it left to right as time. Two horizontal bars are two threads. The amber block is the critical section (the code between lock and unlock). The rule the mutex enforces: the amber blocks never overlap vertically — thread 2 waits (dashed) until thread 1 releases.
Level 1 — Recognition
Goal: name the right tool. No code writing yet.
Recall Solution 1.1
std::lock_guard<std::mutex>. It is the minimal RAII lock: locks in its constructor, unlocks in its destructor, and offers no other operations. "Simplest tool that fits" → lock_guard.
Recall Solution 1.2
std::unique_lock<std::mutex> is required. std::condition_variable::wait must unlock while sleeping and re-lock on wakeup; only unique_lock exposes unlock()/lock(). lock_guard cannot unlock before its destructor, so it cannot participate.
Recall Solution 1.3
t.join()— block the current thread untiltfinishes, then release its resources.t.detach()— lettrun on its own; you give up the ability to wait for it, and it cleans itself up when done. Exactly one must be called on a joinable thread before destruction.
Level 2 — Application
Goal: write correct, compiling code.
Recall Solution 2.1
int counter = 0;
std::mutex mtx;
void add(int N) {
for (int i = 0; i < N; ++i) {
std::lock_guard<std::mutex> lk(mtx); // lock this iteration
++counter; // read-modify-write, now serialized
} // lk destroyed -> unlock
}Two threads, each doing N increments, with every ++counter inside a mutex → no lost updates. Guaranteed final value .
Recall Solution 2.2
std::mutex io_mtx;
void say(const std::string& s) {
std::lock_guard<std::mutex> lk(io_mtx);
std::cout << s << "\n";
}std::cout is one shared object; without the lock two threads' characters interleave. The lock covers the whole (short) scope and never unlocks early → lock_guard is the correct, cheapest choice.
Recall Solution 2.3
std::thread is move-only — it owns one unique OS thread, so copying is deleted. push_back(t) tries to copy → compile error. Fix: move it.
pool.push_back(std::move(t)); // transfer ownership; t is now empty (non-joinable)See Move semantics for why a move leaves t in a valid but resource-less state.
Level 3 — Analysis
Goal: find the bug, count the outcomes, reason about behavior.
Recall Solution 3.1
++counter = read counter into a register → increment the register → write back.
Interleaving that loses an update (start counter = 41):
| time | Thread A | Thread B | counter |
|---|---|---|---|
| 1 | read 41 | 41 | |
| 2 | read 41 | 41 | |
| 3 | inc → 42 | 41 | |
| 4 | inc → 42 | 41 | |
| 5 | write 42 | 42 | |
| 6 | write 42 | 42 |
Both threads incremented, but the final value is 42, not 43 — one increment is lost. This unsynchronized shared write is a data race → Undefined behavior in C++.
Recall Solution 3.2
std::mutex mtx;
void worker() {
std::unique_lock<std::mutex> lk(mtx); // locked
int x = read_shared(); // shared read under lock
lk.unlock(); // release EARLY
heavy(x); // no shared state -> no lock needed
}lock_guard cannot unlock before scope end, so it would hold the lock through all of heavy — blocking others for no reason. unique_lock can unlock() early, so it is mandatory here. Use the flexible tool only when you use the flexibility.
Recall Solution 3.3
Naive body: lock(a.m); lock(b.m); .... Then:
- Thread 1 locks
a, wantsb. - Thread 2 locks
b, wantsa. - Each holds what the other needs → circular wait = deadlock. Neither ever proceeds.

It is a timing bug because it only triggers when both threads grab their first lock before either grabs its second. If thread 1 finishes entirely before thread 2 starts, no deadlock — the program "works" and hides the bug. That intermittency is exactly what makes lock-ordering bugs dangerous. See Deadlock and lock ordering.
Level 4 — Synthesis
Goal: combine tools to solve a real design.
Recall Solution 4.1
void transfer(Account& a, Account& b, int amt) {
std::unique_lock<std::mutex> la(a.m, std::defer_lock); // construct, DON'T lock
std::unique_lock<std::mutex> lb(b.m, std::defer_lock);
std::lock(la, lb); // lock BOTH atomically, deadlock-free
a.bal -= amt;
b.bal += amt;
} // both released by RAIIstd::defer_lock makes each unique_lock adopt but not lock its mutex. std::lock(la, lb) uses a back-off algorithm: it tries to grab all, and if it can't get one it releases any it holds and retries, so no thread sits holding one mutex while waiting for another → the circular-wait condition is broken. (C++17: std::scoped_lock lk(a.m, b.m); does the same in one line.)
Recall Solution 4.2
std::mutex m;
std::condition_variable cv;
std::queue<int> q;
int consume() {
std::unique_lock<std::mutex> lk(m); // unique_lock: cv needs unlock/relock
cv.wait(lk, []{ return !q.empty(); }); // predicate guards spurious wakeups
int v = q.front(); q.pop();
return v;
} // lk releasedunique_lockbecausecv.waitunlockslkwhile sleeping and re-locks on wakeup —lock_guardcan't.- Predicate form
wait(lk, pred)re-checks!q.empty()on every wakeup, so a spurious wakeup (OS wakes the thread with no real notify) doesn't pop an empty queue. See std::condition_variable.
Level 5 — Mastery
Goal: design with tradeoffs; reason about limits and alternatives.
Recall Solution 5.1
Both give the correct final value .
- (a) mutex: every
++counterpays lock + unlock (kernel-mediated contention when threads collide). Correct but heavy for a one-instruction critical section. - (b)
std::atomic<long>:counter.fetch_add(1)(or++counter) is a single lock-free hardware instruction; no blocking, no OS involvement. For a pure counter,std::atomicis faster — mutexes shine when the critical section is more than one trivial op. See std::atomic and lock-free programming.
Recall Solution 5.2
template <class T>
class Guarded {
T data;
std::mutex m;
public:
template <class F>
auto with(F&& f) { // the ONLY way to reach data
std::lock_guard<std::mutex> lk(m); // lock for the whole call
return f(data); // hand a reference to the callback
} // auto-unlock
};
// usage: g.with([](int& x){ ++x; });data is private; the only path to it is with, which locks first. There is no API to get a raw reference that outlives the lock, so "use without locking" won't compile. This is RAII and resource management pushed to its limit: the type system enforces the locking discipline.
Recall Solution 5.3
Use std::async/std::future when a thread's job is to compute and return a value. future::get() transfers the result out with no shared mutable state and no manual mutex — you get the value and any exception, synchronized for you. This removes an entire class of data-race bugs.
Here f.get() returns . See std::async and std::future.
Recall One-line recap of the ladder
L1 name the tool ::: lock_guard simple, unique_lock flexible, join/detach on threads.
L2 write it ::: guard the read-modify-write; threads are move-only.
L3 analyze it ::: ++ is read-inc-write; naive two-lock ordering deadlocks by timing.
L4 synthesize ::: std::lock(defer,defer) beats deadlock; unique_lock+cv for waiting.
L5 master ::: atomic for counters, future for results, Guarded<T> makes locking mandatory.
Connections
- Parent topic (Hinglish)
- RAII and resource management — the mechanism behind every lock wrapper here
- std::atomic and lock-free programming — Exercise 5.1's faster counter
- std::condition_variable — Exercise 4.2's wait-without-spinning
- Deadlock and lock ordering — Exercises 3.3 & 4.1
- std::async and std::future — Exercise 5.3
- Undefined behavior in C++ — what a data race actually is
- Move semantics — why
std::threadis move-only