C++ by Example
C++ by Example

Mutexes and Synchronization

2 min read

Mutexes and Synchronization

When multiple threads access shared data, you need synchronization to prevent data races. A data race — two threads accessing the same memory concurrently when at least one is writing — is undefined behavior in C++.

std::mutex

A mutex (mutual exclusion lock) ensures only one thread executes a protected section at a time:

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;
int counter = 0;

void increment(int n) {
    for (int i = 0; i < n; i++) {
        mtx.lock();
        counter++;
        mtx.unlock();
    }
}

int main() {
    std::thread t1(increment, 100000);
    std::thread t2(increment, 100000);
    t1.join(); t2.join();

    std::cout << counter << "\n";   // 200000 — safe
}

std::lock_guard

Manual lock()/unlock() is error-prone — an exception between them leaves the mutex locked forever. Use lock_guard instead (RAII for mutexes):

void increment_safe(int n) {
    for (int i = 0; i < n; i++) {
        std::lock_guard<std::mutex> lock(mtx);   // locks on construction
        counter++;
    }   // unlocks automatically here — even if an exception is thrown
}

C++17 allows template argument deduction: std::lock_guard lock(mtx).

std::unique_lock

unique_lock is more flexible than lock_guard — it can be unlocked and relocked, supports try_lock, and works with condition variables:

std::unique_lock<std::mutex> lock(mtx);
// ... do work under the lock ...
lock.unlock();
// ... do work without the lock ...
lock.lock();
// ... more work under the lock ...

std::condition_variable

A condition variable lets threads wait efficiently for a condition to become true:

#include <mutex>
#include <condition_variable>
#include <thread>
#include <queue>
#include <iostream>

std::mutex mtx;
std::condition_variable cv;
std::queue<int> work_queue;
bool done = false;

void producer() {
    for (int i = 0; i < 5; i++) {
        {
            std::lock_guard lock(mtx);
            work_queue.push(i);
        }
        cv.notify_one();
    }
    { std::lock_guard lock(mtx); done = true; }
    cv.notify_all();
}

void consumer() {
    while (true) {
        std::unique_lock lock(mtx);
        cv.wait(lock, []{ return !work_queue.empty() || done; });

        while (!work_queue.empty()) {
            std::cout << "processed: " << work_queue.front() << "\n";
            work_queue.pop();
        }

        if (done) break;
    }
}

int main() {
    std::thread p(producer), c(consumer);
    p.join(); c.join();
}

cv.wait(lock, predicate) atomically releases the lock and sleeps until notify_one() or notify_all() is called, then re-checks the predicate (to handle spurious wakeups).

std::atomic

For simple integer operations, atomics are faster than mutexes — no lock required:

#include <atomic>
#include <thread>
#include <iostream>

std::atomic<int> counter{0};

void increment(int n) {
    for (int i = 0; i < n; i++) counter++;
}

int main() {
    std::thread t1(increment, 100000);
    std::thread t2(increment, 100000);
    t1.join(); t2.join();
    std::cout << counter << "\n";   // 200000
}

std::atomic<T> works for basic types (int, bool, pointers). For complex operations, you still need a mutex.

Avoiding deadlocks

A deadlock occurs when two threads each hold a lock the other needs. Rules to prevent it:

  • Always acquire multiple locks in the same order across all threads.
  • Use std::lock(m1, m2) or std::scoped_lock(m1, m2) (C++17) to lock multiple mutexes atomically.
  • Keep critical sections short.
C++ by Example
C++ by Example

Learn modern C++ through working code. Each chapter introduces one concept — variables, functions, classes, templates, smart pointers, concurrency — with a clear example, a line-by-line explanation, and notes on how it applies in real programs.

View book →