[2026] C++ Mutex & Lock — Mutual Exclusion, lock_guard, unique_lock, and Deadlock Prevention

[2026] C++ Mutex & Lock — Mutual Exclusion, lock_guard, unique_lock, and Deadlock Prevention

이 글의 핵심

A practical guide to C++ mutex and locks: mutex basics, lock_guard (RAII), unique_lock, multi-mutex locking, and real-world pitfalls.

Introduction

A mutex is a synchronization primitive for mutual exclusion. It stops multiple threads from accessing shared data at the same time and helps prevent data races.


What you actually see in production

When you learn to program, examples are tidy and theoretical. Production is different: legacy code, tight schedules, and bugs you did not expect. The topics here were learned as theory first; applying them in real projects is when you see why APIs are shaped the way they are.

What stuck with me was a rough patch on an early project. I did what the book said, but it still failed, and I spent days on it. A senior’s code review surfaced the issue, and I learned a lot from that. This article covers not only the mechanics, but traps and fixes you are likely to hit in practice.

1. std::mutex basics

Basic usage

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

// std::mutex: mutual exclusion — only one thread at a time
std::mutex mtx;
int sharedData = 0;  // shared data accessed by multiple threads

void increment() {
    // mtx.lock(): lock the mutex (other threads block until unlock)
    mtx.lock();
    
    // Critical section: at most one thread runs this at a time
    ++sharedData;
    
    // mtx.unlock(): release the mutex so another thread can enter
    mtx.unlock();
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    
    t1.join();
    t2.join();
    
    std::cout << "sharedData: " << sharedData << std::endl;  // 2
    
    return 0;
}

Problem: exception safety

#include <mutex>
#include <stdexcept>
#include <iostream>

std::mutex mtx;

void unsafeFunction() {
    mtx.lock();
    
    // If an exception is thrown, unlock never runs!
    if (someCondition) {
        throw std::runtime_error("error");
    }
    
    mtx.unlock();  // not reached if an exception is thrown
}

int main() {
    try {
        unsafeFunction();
    } catch (...) {
        std::cout << "Exception: mutex stays locked!" << std::endl;
    }
    
    return 0;
}

The core issue: If unsafeFunction throws before unlock, the mutex can remain locked forever. Threads that later wait on the same mutex can stall in a deadlock-like state.


2. lock_guard (RAII)

Automatic lock and unlock

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

std::mutex mtx;
int sharedData = 0;

void safeIncrement() {
    std::lock_guard<std::mutex> lock(mtx);
    ++sharedData;
    // Destructor unlocks automatically
}

int main() {
    std::thread t1(safeIncrement);
    std::thread t2(safeIncrement);
    
    t1.join();
    t2.join();
    
    std::cout << "sharedData: " << sharedData << std::endl;  // 2
    
    return 0;
}

Pattern: lock_guard locks in the constructor and unlocks in the destructor, so release is guaranteed even on return or exception. Keeping the guard’s lifetime as small as possible shrinks the critical section and reduces contention.

Exception safety

#include <mutex>
#include <stdexcept>
#include <iostream>

std::mutex mtx;

void safeFunction() {
    std::lock_guard<std::mutex> lock(mtx);
    
    // Even if this throws, unlock runs in the destructor
    if (someCondition) {
        throw std::runtime_error("error");
    }
    
    // Destructor of lock calls unlock
}

int main() {
    try {
        safeFunction();
    } catch (...) {
        std::cout << "Exception: mutex released automatically" << std::endl;
    }
    
    return 0;
}

3. unique_lock

Flexible locking

#include <mutex>
#include <iostream>

std::mutex mtx;

void flexibleLock() {
    std::unique_lock<std::mutex> lock(mtx);
    
    std::cout << "work 1" << std::endl;
    
    lock.unlock();
    
    std::cout << "work without lock" << std::endl;
    
    lock.lock();
    
    std::cout << "work 2" << std::endl;
    
    // Destructor unlocks if still held
}

int main() {
    flexibleLock();
    return 0;
}

Deferred locking

#include <mutex>
#include <iostream>

std::mutex mtx;

void deferredLock() {
    // Do not lock in the constructor
    std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
    
    if (needLock) {
        lock.lock();
    }
    
    // perform work
}

int main() {
    deferredLock();
    return 0;
}

4. Multiple mutexes

Deadlock risk

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

std::mutex mtx1, mtx2;
int balance1 = 100, balance2 = 200;

// Bad: deadlock possible
void badTransfer() {
    // Thread 1: mtx1 then mtx2
    mtx1.lock();
    mtx2.lock();
    
    balance1 -= 50;
    balance2 += 50;
    
    mtx2.unlock();
    mtx1.unlock();
}

void anotherBadTransfer() {
    // Thread 2: mtx2 then mtx1 (deadlock!)
    mtx2.lock();
    mtx1.lock();
    
    balance2 -= 30;
    balance1 += 30;
    
    mtx1.unlock();
    mtx2.unlock();
}

Using std::lock

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

std::mutex mtx1, mtx2;
int balance1 = 100, balance2 = 200;

// Good: std::lock avoids deadlock
void safeTransfer() {
    std::lock(mtx1, mtx2);  // lock both atomically
    
    std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
    std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
    
    balance1 -= 50;
    balance2 += 50;
    
    std::cout << "transfer done: " << balance1 << ", " << balance2 << std::endl;
}

int main() {
    std::thread t1(safeTransfer);
    std::thread t2(safeTransfer);
    
    t1.join();
    t2.join();
    
    return 0;
}

scoped_lock (C++17)

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

std::mutex mtx1, mtx2;

// C++17: scoped_lock — concise multi-mutex RAII
void modernTransfer() {
    std::scoped_lock lock(mtx1, mtx2);
    
    std::cout << "performing transfer" << std::endl;
    
    // Destructor unlocks both
}

int main() {
    std::thread t1(modernTransfer);
    std::thread t2(modernTransfer);
    
    t1.join();
    t2.join();
    
    return 0;
}

5. Mutex types

recursive_mutex

#include <mutex>
#include <iostream>

std::recursive_mutex rmtx;

void func1() {
    std::lock_guard<std::recursive_mutex> lock(rmtx);
    std::cout << "func1" << std::endl;
}

void func2() {
    std::lock_guard<std::recursive_mutex> lock(rmtx);
    std::cout << "func2" << std::endl;
    func1();  // OK: recursive lock on same mutex
}

int main() {
    func2();
    return 0;
}

timed_mutex

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

std::timed_mutex tmtx;

void tryLockFor() {
    if (tmtx.try_lock_for(std::chrono::milliseconds(100))) {
        std::cout << "lock acquired" << std::endl;
        
        std::this_thread::sleep_for(std::chrono::milliseconds(50));
        
        tmtx.unlock();
    } else {
        std::cout << "lock failed (timeout)" << std::endl;
    }
}

int main() {
    std::thread t1(tryLockFor);
    std::thread t2(tryLockFor);
    
    t1.join();
    t2.join();
    
    return 0;
}

6. Practical example: threads

Analogy: Concurrency is like one cook switching between tasks on several dishes. Parallelism is several cooks working on different dishes at once.

Thread-safe counter

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

class ThreadSafeCounter {
    mutable std::mutex mtx;
    int count = 0;
    
public:
    void increment() {
        std::lock_guard<std::mutex> lock(mtx);
        ++count;
    }
    
    void decrement() {
        std::lock_guard<std::mutex> lock(mtx);
        --count;
    }
    
    int get() const {
        std::lock_guard<std::mutex> lock(mtx);
        return count;
    }
    
    void reset() {
        std::lock_guard<std::mutex> lock(mtx);
        count = 0;
    }
};

int main() {
    ThreadSafeCounter counter;
    std::vector<std::thread> threads;
    
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back([&counter]() {
            for (int j = 0; j < 1000; ++j) {
                counter.increment();
            }
        });
    }
    
    for (auto& t : threads) {
        t.join();
    }
    
    std::cout << "final count: " << counter.get() << std::endl;  // 10000
    
    return 0;
}

Summary

Takeaways

  1. mutex: mutual exclusion for shared data
  2. lock_guard: simple RAII lock
  3. unique_lock: flexible lock with manual control
  4. scoped_lock: multiple mutexes (C++17)
  5. recursive_mutex: re-entrant locking on the same thread
  6. timed_mutex: try-lock with a time limit

Lock comparison

LockCharacteristicsOverheadWhen to use
lock_guardSimple, RAIILowDefault choice
unique_lockFlexible, manual controlMediumConditional locking, std::condition_variable
scoped_lockMultiple mutexesLowMultiple mutexes (C++17)

Practical tips

Guidelines:

  • Prefer lock_guard by default.
  • Use unique_lock when you need manual lock/unlock or condition variables.
  • For several mutexes, prefer scoped_lock (C++17).
  • Use recursive_mutex only when re-entrant locking is truly required.

Deadlock avoidance:

  • Use a consistent global order for locking mutexes.
  • Use std::lock for atomic multi-lock.
  • Prefer scoped_lock (C++17).
  • Keep critical sections short.

Performance:

  • Keep critical sections minimal.
  • lock_guard is usually the cheapest.
  • Use unique_lock only when necessary.
  • For read-heavy workloads, consider shared_mutex (C++17).

Next steps