Concurrency in Modern C++

Course 3 · Ch 4
Concurrency in Modern C++
c3-3's own pthreads material, revisited — a real ergonomic win, but the same honest gap against Rust remains

c3-3 built a race condition with raw pthreads. This chapter shows modern C++'s own equivalents — genuinely nicer to use, but not a different safety story.

std::thread

A higher-level replacement for pthread_create/pthread_join — but with a real, important gotcha: a std::thread still joinable when its destructor runs calls std::terminate. It does not automatically join or detach the way an RAII-styled course might suggest; .join() or .detach() must be called explicitly before destruction.

std::thread t([]() { std::cout << "Hello from a thread\n"; }); t.join(); // required — must be called before t is destroyed

A Worked Example — Revisiting c3-3's Race Condition

int counter = 0; void increment() { for (int i = 0; i < 100000; i++) counter++; } std::thread t1(increment), t2(increment); t1.join(); t2.join(); std::cout << counter << std::endl; // same non-deterministic race as c3-3, just different syntax

The exact same race — the higher-level wrapper changes nothing about the underlying problem, only the syntax used to create the threads.

std::mutex and std::lock_guard

std::mutex is the same concept as c3-3's own pthread_mutex_t. std::lock_guard<std::mutex> is a genuine RAII wrapper around locking — the direct fix to c3-3's own "forgotten unlock deadlocks" warning. Its destructor unlocks automatically, on any exit path, including an exception — exactly cpp2-6's own exception-safety mechanism, applied specifically to locking.

std::mutex lock; void increment() { for (int i = 0; i < 100000; i++) { std::lock_guard<std::mutex> guard(lock); // locks now, unlocks automatically at scope exit counter++; } }

std::atomic

A genuinely different, lower-overhead alternative for simple shared counters and flags specifically.

std::atomic<int> counter{0}; counter++; // a single atomic operation — no mutex needed at all

Resolves c3-3's own race condition with no lock/unlock pairing whatsoever — a real, meaningful alternative worth knowing for exactly this narrow case.

Contrasted With Rust's Send and Sync, Revisited

std::mutex/std::lock_guard are a genuine ergonomic improvement over c3-3's raw pthread_mutex_t — automatic unlocking via RAII, no more forgotten-unlock deadlocks. But the same fundamental gap from c3-3 remains: nothing in C++'s type system ties a std::mutex to the specific data it protects, and nothing prevents accessing that data without holding the lock at all. Rust's Mutex<T> — wrapping the data itself, checked by the Send/Sync marker traits at compile time — is still the genuinely stronger guarantee. lock_guard is a real ergonomic win, not a safety win at the type-system level.

Conceptpthreads (c3-3)Modern C++Rust
Thread creationpthread_create/joinstd::thread, .join()/.detach()std::thread::spawn
Lockingmanual lock/unlock, forgettablestd::lock_guard — RAII, automatic unlockMutex<T> wraps the data itself
Mutex-to-data relationshipnone — separate variablesstill none — separate variablesenforced by the type system
Accessing data without lockingcompiles, UB at runtimecompiles, UB at runtimenot possible — no name refers to the data directly
Prefer std::atomic for simple cases, lock_guard for the rest
Use std::atomic for simple counters/flags; std::lock_guard (or std::unique_lock for more flexibility) for anything needing genuine mutex protection. Avoid raw lock()/unlock() pairs in new code entirely.
A joinable std::thread destroyed without join/detach crashes the program
Unlike lock_guard, std::thread does not solve this via RAII — a thread still joinable when destroyed calls std::terminate immediately. Genuinely easy to forget on an early return or exception path, echoing cpp2-6's own exception-safety themes, but not automatically handled here the way locking is.

Coding Challenges

Challenge 1

Rewrite this chapter's own counter race using std::thread instead of pthreads, run it a few times, and report whether the final value is consistently 200,000.

📄 View solution
Challenge 2

Fix Challenge 1 two different ways: once using std::lock_guard around the increment, and once using std::atomic instead of a plain int with no lock at all. Confirm both produce a consistent, correct 200,000.

📄 View solution
Challenge 3

Explain precisely why std::lock_guard is described as a real ergonomic improvement over c3-3's raw pthread_mutex_t but NOT a safety improvement at the type-system level, tying your answer to what Rust's Mutex<T> does differently.

📄 View solution

Chapter 4 Quick Reference

  • std::thread — must be explicitly .join()ed or .detach()ed, or its destructor calls std::terminate
  • std::lock_guard<std::mutex> — RAII locking, unlocks automatically on any exit path including exceptions
  • std::atomic<T> — lock-free, for simple counters/flags specifically
  • Modern C++'s tools are a real ergonomic improvement over raw pthreads — not a type-system safety improvement
  • The mutex-to-data gap from c3-3 remains — Rust's Mutex<T> still closes it, C++'s doesn't
  • Next chapter: undefined behavior in C++ — building on c3-4's own C-level UB catalog