5.2.24C++ Programming

Concurrency — std - thread, std - mutex, std - lock_guard, std - unique_lock

1,841 words8 min readdifficulty · medium

WHY does concurrency exist at all?


WHAT are the four tools?


HOW to use them — built from first principles

Step 1: A data race (the disease)

int counter = 0;
void add() { for (int i = 0; i < 100000; ++i) ++counter; }
 
int main() {
    std::thread t1(add), t2(add);
    t1.join(); t2.join();
    std::cout << counter; // NOT reliably 200000 — data race!
}

Why this breaks: ++counter is really read → increment → write. Two threads interleave these steps, so increments get lost.

Step 2: The cure — protect with a mutex

int counter = 0;
std::mutex mtx;
void add() {
    for (int i = 0; i < 100000; ++i) {
        std::lock_guard<std::mutex> lk(mtx); // locks here
        ++counter;
    } // lk destroyed → unlock here, every iteration
}

Why this works: at most one thread is inside the guarded region; ++counter becomes effectively atomic. Result is always 200000.

Figure — Concurrency — std - thread, std - mutex, std - lock_guard, std - unique_lock

Worked examples


Common mistakes


Flashcards

What is a data race?
Two threads access the same memory location concurrently, at least one writes, and there's no synchronization → undefined behavior.
Why prefer lock_guard over manual lock()/unlock()?
RAII guarantees unlock runs on every scope exit, including exceptions and early returns, preventing deadlock.
What happens if a joinable std::thread is destroyed without join/detach?
std::terminate() is called, aborting the program.
Difference between lock_guard and unique_lock?
lock_guard is minimal: locks at construction, unlocks at destruction, no early control. unique_lock is flexible: defer/early unlock/relock, movable, works with condition variables.
Why does unique_lock (not lock_guard) pair with std::condition_variable?
cv.wait must unlock while sleeping and re-lock on wakeup, which only unique_lock allows.
How do you lock two mutexes without risking deadlock?
Construct both with std::defer_lock, then std::lock(a, b) (or std::scoped_lock(a,b) in C++17) to acquire all-or-nothing.
Is std::thread copyable?
No, it is move-only; it owns a unique OS thread resource.
What does ++counter actually do at the hardware level?
Read counter, increment, write back — three steps that can interleave between threads.

Recall Feynman: explain it to a 12-year-old

Imagine two kids sharing one coloring book. If they both scribble at once, the picture turns to mush — that's a data race. So you make a rule: there's only one crayon (the mutex), and you must grab it before coloring and put it back after. lock_guard is a magic clip that automatically drops the crayon the moment you walk away, so you can never accidentally hog it. unique_lock is a fancier clip that also lets you put the crayon down early if you want a break. And a thread is just inviting a second kid to color with you — but you must say "okay, you're done now" (join) before cleaning up the table, or you'll knock everything over.


Connections

  • RAII and resource management — lock_guard/unique_lock are RAII applied to locks
  • std::atomic and lock-free programming — alternative to mutex for simple counters
  • std::condition_variable — requires unique_lock
  • Deadlock and lock ordering — why std::lock/scoped_lock exist
  • std::async and std::future — higher-level task launching vs raw std::thread
  • Undefined behavior in C++ — data races are formally UB
  • Move semantics — why std::thread is move-only

Concept Map

enable

launches cooks

touch

unsynchronized causes

is

serializes access to

locks and unlocks

flexibly locks

uses

uses

prevents

works with

Multiple CPU cores

Concurrency

std thread

Shared memory access

Data race

Undefined behavior

std mutex

std lock_guard

std unique_lock

RAII scope exit unlock

Deadlock

condition_variable

Hinglish (regional understanding)

Intuition Hinglish mein samjho

Dekho, concurrency ka matlab hai ki tumhare program me ek se zyada threads ek saath chal rahe hain, kyunki aaj ke CPU me multiple cores hote hain. std::thread t(func) likhte hi naya thread launch ho jaata hai. Lekin yaad rakhna — thread object destroy hone se pehle tumhe t.join() (wait karo) ya t.detach() (chhod do) karna zaroori hai, warna program std::terminate se crash kar jaata hai.

Problem tab aati hai jab do threads ek hi shared variable ko chhute hain aur koi ek usse likhta (write) bhi hai. Jaise ++counter actually teen kaam hai — read, increment, write. Do threads beech me interleave ho jaate hain aur kuch increments lost ho jaate hain. Isko data race kehte hain, aur C++ me yeh undefined behavior hai — kuch bhi ho sakta hai.

Iska ilaaj hai std::mutex — ek aisi cheez jise ek time me sirf ek thread "lock" kar sakta hai. Baaki threads lock() pe ruk jaate hain jab tak woh free na ho. Par mutex ko haath se lock()/unlock() mat karo — agar beech me exception ya early return aa gaya to unlock chhoot jaayega aur permanent deadlock ho jaayega. Isliye hamesha std::lock_guard use karo: yeh constructor me lock karta hai aur scope khatam hote hi destructor me unlock — yeh kabhi skip nahi hota. RAII ka jaadu!

Aur jab zyada flexibility chahiye — jaise lock ko early release karna, ya condition_variable ke saath kaam karna — tab std::unique_lock use karo. Do mutex ek saath lene ho to std::defer_lock + std::lock(a,b) use karo taaki deadlock na ho. Simple rule: simple kaam me lock_guard, flexible kaam me unique_lock.

Go deeper — visual, from zero

Test yourself — C++ Programming

Connections