Appearance
C++ Concurrency, Atomics, and the Memory Model
Threads share an address space, but ordinary reads and writes are not automatically safe. If two threads access the same memory concurrently, at least one access writes, and the accesses are not properly synchronized, the program has a data race and undefined behavior.
Threads need lifetime and failure ownership
cpp
#include <iostream>
#include <thread>
int main() {
int result = 0;
std::jthread worker{[&result] { result = 42; }};
worker.join();
std::cout << result << '\n';
}The join establishes that the worker finishes before the read. C++20 jthread joins automatically at destruction and supports cooperative stop tokens. A std::thread must be joined or detached before its destructor; destroying a joinable thread terminates the process.
Detached threads make ownership and shutdown difficult. Prefer structured objects or executors whose lifetime encloses their work.
Mutexes protect invariants
cpp
std::mutex mutex;
std::vector<Job> queue;
void enqueue(Job job) {
std::lock_guard lock{mutex};
queue.push_back(std::move(job));
}Protect the invariant, not merely one variable. Use RAII locks so exceptions and returns release the mutex. Avoid calling unknown code while holding a lock; it can block, reenter, or acquire locks in another order.
Condition variables wait for state changes. Always test a predicate in a loop or use the predicate overload because wakeups can be spurious and another thread may consume the condition first.
Happens-before makes writes visible
Synchronization creates ordering relationships. Unlocking a mutex and later locking the same mutex, joining a thread, and suitable atomic operations can establish happens-before relationships. Without one, hardware and compiler reordering plus cache behavior invalidate simple “thread A ran first” stories.
volatile does not establish inter-thread ordering or atomicity. It serves special observable-access domains such as memory-mapped I/O according to implementation/platform rules.
Atomics are not just indivisible integers
std::atomic operations avoid data races for the atomic object and carry a memory ordering. Sequential consistency is the easiest default. Acquire/release and relaxed orderings can improve specialized algorithms but require a proof about published data and ordering.
cpp
std::atomic<bool> ready{false};Making a flag atomic does not automatically protect a larger non-atomic invariant. Lock-free algorithms are difficult to design, reclaim, and test. Use mutexes unless measurement and architecture justify lower-level atomics.
Async abstractions do not erase scheduling policy
Futures communicate eventual results and exceptions. std::async launch policy has subtleties; thread pools and platform frameworks may be more suitable for many small jobs. The standard library does not provide one batteries-included application executor across all current baselines.
Bound concurrency deliberately, define shutdown and cancellation, and avoid letting background work outlive dependencies it borrows.
Optional prompts
Explain: Why does volatile bool ready not safely publish data to another thread?
Answer:
volatiledoes not create atomic operations or happens-before relationships. Use a mutex or correctly ordered atomic protocol.
Debug: A std::thread reaches scope while still joinable and the application terminates. Is this a leak?
Answer: No; it is the specified destructor behavior. Join, transfer ownership, or use a
jthread/structured owner.