Skip to content

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.

condition.wait(lock, [&] { return closed || !queue.empty(); });

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

  1. Explain the race in two threads executing ++ordinary_counter.
  2. Design a predicate-based bounded blocking queue.
  3. State why a shared pointer protects ownership but not the pointee's state.