C++ from Basics to Mastery — Part 20
Handle concurrency with threads, atomics, and the memory model
The articles on this blog are written for teaching purposes. The information may contain mistakes, so verify it against your target compiler and standard library.
When threads access the same memory, the problem is larger than an unpredictable execution order. Under the C++ memory model, an unsynchronized data race is undefined behavior.
Protect shared invariants explicitly
#include <mutex>
class Inventory
{
public:
void Add(Item item)
{
std::lock_guard lock(Mutex);
Items.push_back(std::move(item));
}
private:
std::mutex Mutex;
std::vector<Item> Items;
};
std::lock_guard releases the mutex when its scope ends, including exception paths. The lock must protect the complete invariant, not just one vector call.
Acquiring multiple mutexes in inconsistent orders can deadlock. Lock them together with std::scoped_lock where appropriate, or establish a project-wide lock order.
Atomics fit small independent values
#include <atomic>
std::atomic<unsigned int> completedJobs{0};
void MarkCompleted()
{
completedJobs.fetch_add(1, std::memory_order_relaxed);
}
Relaxed ordering can be enough for a statistics counter that does not publish other state. One atomic counter does not make a multi-field object consistent.
The default sequentially consistent ordering provides an easier global order. Weaker acquire, release, and relaxed operations should follow a proven happens-before design, not a performance guess.
Tie thread shutdown to lifetime with jthread
#include <stop_token>
#include <thread>
void Worker(std::stop_token stop)
{
while (!stop.stop_requested())
{
ProcessOneJob();
}
}
void Run()
{
std::jthread worker(Worker);
// Destruction requests stop and joins the thread.
}
C++20 std::jthread requests cancellation and waits for completion during destruction. A stop_token is cooperative: blocking I/O still needs a timeout or another wake-up mechanism.
Prefer designs that remove shared mutation. Give workers independent inputs and collect results through messages or a queue. Even separate atomics can suffer false sharing when cores repeatedly modify values on the same cache line.
One successful test run does not prove concurrent code safe. Document ownership, shutdown, lock order, and publication rules, then exercise them with ThreadSanitizer and repeatable stress tests.