5.2.24 · D5C++ Programming

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

1,729 words8 min readBack to topic

Before the questions, one shared vocabulary reminder so no term below is used before it is grounded:


True or false — justify

A mutex makes the variable it "protects" atomic on its own.
False — a mutex protects nothing by itself; it only helps if every thread agrees to lock the same mutex before touching that variable. Forget one access and the race returns. See std::atomic and lock-free programming.
Locking a mutex guarantees your code is correct.
False — it removes data races, but you can still have deadlocks, wrong lock ordering, or forget to lock some path. Correctness is your job; the mutex only serializes access.
std::lock_guard and std::unique_lock do the same thing at the same speed.
False — lock_guard is minimal and slightly cheaper; unique_lock carries an extra "am I currently holding it?" flag to support early unlock/relock and moving, so use the flexible one only when you need flexibility.
A std::thread object is the running thread.
False — the object is a handle to the OS thread. The thread can outlive expectations; the handle owns and must dispose of that resource via join() or detach().
Calling detach() means the thread's work definitely finishes before main ends.
False — a detached thread runs independently, so if main returns first the process exits and the detached thread may be killed mid-work with no cleanup.
Two threads reading (never writing) the same variable is a data race.
False — concurrent reads with no writer are safe; a race needs at least one writer. Once any thread writes, all accesses must be synchronized.
lock_guard can be returned from a function to transfer the lock to the caller.
False — lock_guard is non-movable and non-copyable, so it cannot leave its scope. unique_lock is movable and can be returned to hand off ownership. See Move semantics.
If code between lock() and unlock() throws, RAII still releases the lock.
True — that is the whole point: stack unwinding runs the guard's destructor, which unlocks, even on an exception or early return.

Spot the error

std::lock_guard lk(mtx); lk.unlock(); heavy_work(); — releasing early to unblock others.
lock_guard has no unlock() member — this won't compile. To release early you must use std::unique_lock, whose unlock() exists exactly for this case.
Thread 1 locks a then b; thread 2 locks b then a.
Classic deadlock: each holds one mutex and waits forever for the other. Fix with a consistent global lock order, or std::lock(a,b) / std::scoped_lock(a,b). See Deadlock and lock ordering.
std::thread t(work); /* ... */ and the function returns without t.join().
A still-joinable thread destroyed at scope exit calls std::terminate(), killing the whole program. You must join() or detach() (or use std::jthread) first.
std::thread t2 = t1; to run the same work on two handles.
A thread is a unique resource and is move-only, so copy-assignment won't compile. Use std::thread t2 = std::move(t1); — but then t1 no longer owns a thread.
Guarding only the write to a shared counter while other threads read it unlocked.
Still a data race — the unlocked reads run concurrently with a locked write. Every access to the shared data, reads included, must take the same mutex.
std::mutex m; std::lock_guard lk1(m); std::lock_guard lk2(m); in one thread.
A plain std::mutex is non-recursive: locking it twice in the same thread deadlocks that thread against itself. Use std::recursive_mutex only if re-entrant locking is truly needed (usually a design smell).
Using cv.wait(lk, pred) where lk is a std::lock_guard.
Won't compile — condition_variable::wait needs a std::unique_lock because it must unlock while sleeping and re-lock on wakeup, which lock_guard cannot do. See std::condition_variable.
Locking a mutex, then inside the critical section calling a function that locks the same mutex.
Self-deadlock (same non-recursive mutex, same thread). The nested call blocks waiting for a lock its own thread already holds and will never release.

Why questions

Why is ++counter unsafe across threads even though it's "one line" of C++?
One line of source is three machine steps — read, increment, write — and two threads can interleave those steps so an increment is lost. Atomicity of source lines is an illusion.
Why does RAII (lock_guard) prevent deadlocks that manual unlock() invites?
A destructor runs on every scope exit — normal, early return, break, or exception — so the unlock can never be skipped, whereas a hand-written unlock() sits after code that might jump past it.
Why hold a lock for the shortest time possible?
While you hold it, every other thread wanting that mutex is blocked, serialized behind you. Long critical sections throw away the parallelism concurrency was meant to buy.
Why is a data race "undefined behavior" rather than just "a wrong number"?
The C++ memory model gives no meaning at all to unsynchronized concurrent access, so the compiler may reorder, cache, or optimize as if it never happens — the program can do literally anything. See Undefined behavior in C++.
Why does std::lock(la, lb) avoid the deadlock that locking them in order does not?
It uses a lock-and-back-off algorithm that acquires all mutexes or none, so it never sits holding one while waiting on another — removing the circular-wait condition. See Deadlock and lock ordering.
Why is std::thread move-only instead of copyable?
It owns exactly one underlying OS thread — a unique, non-duplicable resource. Copying would imply two owners of one thread, which is meaningless, so only ownership transfer (move) is allowed. See Move semantics.
Why prefer std::atomic<int> over a mutex for a simple shared counter?
For a single primitive, a lock-free atomic increment avoids the overhead and blocking of a mutex while still being race-free — the mutex is overkill. See std::atomic and lock-free programming.
Why does releasing a unique_lock early (lk.unlock()) before heavy, lock-free work help?
The heavy work doesn't touch shared state, so holding the lock through it would needlessly block other threads. Unlocking first lets them proceed in parallel.

Edge cases

What happens if you join() a thread that is already joined or was detached?
Undefined behavior / a thrown exception, because the handle no longer owns a joinable thread. Always check t.joinable() or track state before calling join().
Can two different mutexes ever cause a deadlock?
Yes — deadlock is about ordering, not identity. If two threads acquire two distinct mutexes in opposite orders, they deadlock. See Deadlock and lock ordering.
Is it safe to lock a mutex in one thread and unlock it from a different thread?
No — a std::mutex must be unlocked by the same thread that locked it; doing otherwise is undefined behavior. (This is also why you can't hand a raw lock_guard across threads.)
What if main returns while a detached thread is still writing to a global?
The process tears down; the detached thread may be accessing memory that is being destroyed → undefined behavior. Detach only threads whose lifetime you're sure ends safely.
Does an empty (default-constructed) std::thread need join()?
No — it owns no thread, joinable() is false, and its destructor is harmless. Only a joinable thread requires join()/detach().
What is the state of a std::thread after you move from it?
It becomes non-joinable (owns nothing), so it needs no join(), and the destination now owns and must dispose of the thread. See Move semantics.
Zero threads, one thread — is a mutex still needed for a single-threaded program?
No — with no concurrency there is no race, so the locking is pure overhead. Mutexes matter only when memory is genuinely shared across threads.

Recall One-line self-test before you leave

If you can answer "which mutex protects this variable, who locks it, and when is it released?" for every shared access in your code — and "is this thread joined, detached, or moved-from?" for every std::thread — you have internalized this whole page.


Connections

  • RAII and resource management — why destructor-based unlock beats manual unlock
  • Deadlock and lock ordering — the ordering traps in "Spot the error"
  • std::condition_variable — why unique_lock, not lock_guard
  • std::atomic and lock-free programming — mutex-free alternative for simple counters
  • Undefined behavior in C++ — what "data race" actually means
  • Move semantics — why std::thread and unique_lock are move-only