Concurrency in C++¶
C++ concurrency combines threads, mutexes, condition variables, futures, atomics, and a formal memory model. A data race on ordinary memory produces undefined behavior.
Thread lifetime¶
std::jthread is a C++20 joining thread: destruction requests stop and joins
when joinable. Cooperative cancellation requires the task to observe a stop token.
void consume(std::stop_token stop, work_queue& queue) {
while (!stop.stop_requested()) {
if (auto item = queue.try_pop()) {
process(*item);
}
}
}
std::jthread worker{consume, std::ref(queue)};
Object lifetimes still matter: referenced state must outlive every access by the thread. Joining does not repair a reference that already dangled.
Mutex-protected invariant¶
class counter {
public:
void increment() {
std::lock_guard guard{mutex_};
++value_;
}
[[nodiscard]] std::uint64_t value() const {
std::lock_guard guard{mutex_};
return value_;
}
private:
mutable std::mutex mutex_;
std::uint64_t value_{};
};
The mutex and guarded state share one owner. Every access follows the same locking protocol. Multiple individually locked calls are not automatically one atomic compound operation.
Condition variables¶
Wait with a predicate because wakeups may be spurious and another thread may consume the condition before a waiter reacquires the lock.
The predicate reads state protected by the same mutex. Notification is not stored as an event; the protected condition is the truth being waited for.
Atomics and memory ordering¶
std::atomic<T> provides atomic operations for supported types. The default
sequentially consistent ordering is easiest to reason about. Weaker orderings can
improve selected algorithms but require a proof connecting synchronization,
object lifetime, and every non-atomic access.
Atomicity of one variable does not make a multi-variable invariant atomic. Lock- free does not mean wait-free, faster, or simple. Prefer established concurrent structures and measure before implementing low-level lock-free algorithms.
volatile is not synchronization¶
In portable C++, volatile does not make compound operations atomic and does not
create the inter-thread ordering needed to avoid a data race. It has specialized
uses such as certain hardware access, subject to platform contracts.
Parallel computation¶
Parallel algorithms and tasks help only when work is sufficiently large and independent. Sequential fractions, scheduling, contention, false sharing, cache and memory bandwidth, and oversubscription bound speedup. Benchmark the whole workload with correct results checks.
Exercises¶
- Explain the race in two threads executing
++ordinary_counter. - Design a predicate-based bounded blocking queue.
- State why a shared pointer protects ownership but not the pointee's state.