Concurrency Basics
Concurrency Basics
Start threads safely, protect shared data, and avoid common synchronization mistakes.
Concurrency Basics
Start threads safely, protect shared data, and avoid common synchronization mistakes.
std::thread worker([] {
do_work();
});
worker.join();
This is the bare minimum shape, but in modern code you usually want ownership and shutdown rules to be explicit rather than relying on every exit path calling join() correctly.
std::mutex m;
int total = 0;
void add() {
std::lock_guard<std::mutex> lock(m);
++total;
}
The lock protects the invariant around total. If several variables must stay in sync, protect the group with one rule rather than sprinkling locks around individual reads and writes.
std::jthread for owned worker threadsstd::jthread worker([](std::stop_token stop) {
while (!stop.stop_requested()) {
poll_once();
}
});
std::jthread joins on destruction and supports cooperative cancellation through std::stop_token, which makes thread lifetime much easier to reason about.
std::mutex guard;
std::condition_variable cv;
std::queue<int> jobs;
bool done = false;
void producer() {
{
std::scoped_lock lock(guard);
jobs.push(42);
}
cv.notify_one();
}
void consumer() {
std::unique_lock lock(guard);
cv.wait(lock, [&] { return done || !jobs.empty(); });
}
This is the first pattern worth learning after plain mutex locking because it introduces sleeping, wakeup conditions, and coordination around a shared queue.
std::mutex queue_mutex;
std::condition_variable queue_cv;
std::queue<int> jobs;
bool done = false;
void producer() {
for (int value = 1; value <= 5; ++value) {
{
std::scoped_lock lock(queue_mutex);
jobs.push(value);
}
queue_cv.notify_one();
}
{
std::scoped_lock lock(queue_mutex);
done = true;
}
queue_cv.notify_all();
}
void worker(std::stop_token stop) {
while (!stop.stop_requested()) {
std::unique_lock lock(queue_mutex);
queue_cv.wait(lock, [&] { return done || !jobs.empty(); });
if (done && jobs.empty()) {
break;
}
int value = jobs.front();
jobs.pop();
lock.unlock();
std::cout << "processed " << (value * 2) << '\n';
}
}
This example shows the real concurrency workflow: one thread produces work, workers sleep efficiently until notified, and shutdown is explicit instead of accidental.
Also avoid detached threads unless you have a very deliberate lifetime model. Detached work is easy to start and hard to shut down or observe correctly.
Also avoid holding the queue lock while doing the expensive work itself. Lock only long enough to move shared state safely, then release it.
Make correctness obvious first. Parallel speedups matter only after the sequential design is correct.
Always wait with a predicate so spurious wakeups do not break correctness.
cv.wait(lock, [] { return ready; });
The predicate is the actual rule. notify_one() and notify_all() are only hints that a waiting thread should re-check it.
Use atomics for small shared states such as counters, flags, and state transitions. Reach for mutexes when you need to protect larger invariants.
std::mutex.std::condition_variable with a queue.std::jthread.std::atomic.If you ever need more than one mutex, define a lock ordering rule early and keep it consistent. Many concurrency bugs come from individually correct locks taken in different orders.
std::thread example to std::jthread and add a stop request.#include <thread>
int main() {
std::jthread worker([] { /* do work */ });
return 0;
}
Add a stop condition or notification path. Concurrency examples become useful only once shutdown and coordination are explicit.
#include <stop_token>
#include <thread>
int main() {
std::jthread worker([](std::stop_token token) {
while (!token.stop_requested()) {
break;
}
});
}