[2026] C++ scoped_lock — Scoped locking, std::lock, and deadlock avoidance

[2026] C++ scoped_lock — Scoped locking, std::lock, and deadlock avoidance

이 글의 핵심

std::scoped_lock (C++17) is an RAII lock that locks multiple mutexes at once using std::lock. Covers differences from lock_guard and unique_lock, deadlock avoidance, multi-lock patterns, and performance.

What is scoped_lock?

std::scoped_lock (C++17) is an RAII lock that locks multiple mutexes at once and automatically unlocks all of them when the scope ends. It is the multi-mutex counterpart of lock_guard and always acquires mutexes in a consistent order to help prevent deadlocks. Reading thread basics and data races and mutexes first is recommended.

Example implementation of func:

#include <mutex>

std::mutex m1, m2;

void func() {
    std::scoped_lock lock(m1, m2);  // deadlock avoidance
    // ...
}

Why it matters:

  • Deadlock avoidance: safely locks multiple mutexes
  • RAII: automatic unlock for exception safety
  • Simplicity: replaces more verbose C++11 patterns
  • Generality: supports both single and multiple mutexes

Example implementations of transfer1 and transfer2:

// ❌ Manual locking: deadlock risk
std::mutex m1, m2;

void transfer1() {
    m1.lock();
    m2.lock();  // can deadlock!
    // ...
    m2.unlock();
    m1.unlock();
}

void transfer2() {
    m2.lock();
    m1.lock();  // can deadlock!
    // ...
    m1.unlock();
    m2.unlock();
}

// ✅ scoped_lock: deadlock avoidance
void transfer1() {
    std::scoped_lock lock(m1, m2);  // safe
    // ...
}

void transfer2() {
    std::scoped_lock lock(m2, m1);  // safe (order does not matter)
    // ...
}

How scoped_lock works

Internally, scoped_lock uses std::lock() and a deadlock-avoidance algorithm to lock all mutexes. The destructor unlocks every mutex automatically.

// Conceptual implementation
// runnable-style example
template<typename....Mutexes>
class scoped_lock {
    std::tuple<Mutexes&...> mutexes_;
    
public:
    scoped_lock(Mutexes&....mutexes) : mutexes_(mutexes...) {
        std::lock(mutexes...);  // deadlock-avoidance algorithm
    }
    
    ~scoped_lock() {
        // unlock in reverse order
        unlock_all(mutexes_);
    }
    
    // not copyable or movable
    scoped_lock(const scoped_lock&) = delete;
    scoped_lock& operator=(const scoped_lock&) = delete;
};

Deadlock-avoidance algorithm

std::lock() uses a try-lock-based algorithm to avoid deadlock:

  1. Lock the first mutex.
  2. Try to lock the rest with try_lock().
  3. On failure, unlock all and retry.
  4. Repeat until all locks succeed.
// Conceptual behavior of std::lock
// runnable-style example
void lock(Mutex& m1, Mutex& m2) {
    while (true) {
        m1.lock();
        if (m2.try_lock()) {
            return;  // success
        }
        m1.unlock();  // failed, retry
        
        std::this_thread::yield();
    }
}

Basic usage

Example implementation of func:

std::mutex mtx;

void func() {
    std::scoped_lock lock(mtx);
    // automatic lock/unlock
}

Practical examples

Example 1: bank transfer

class Account {
    std::mutex mtx;
    int balance;
    
public:
    Account(int b) : balance(b) {}
    
    friend void transfer(Account& from, Account& to, int amount) {
        // deadlock avoidance
        std::scoped_lock lock(from.mtx, to.mtx);
        
        from.balance -= amount;
        to.balance += amount;
    }
    
    int getBalance() const {
        std::scoped_lock lock(mtx);
        return balance;
    }
};

Example 2: multiple resources

Example implementation of update:

std::mutex m1, m2, m3;
int data1, data2, data3;

void update() {
    std::scoped_lock lock(m1, m2, m3);
    
    data1++;
    data2++;
    data3++;
}

Example 3: single mutex

Example implementation of func:

std::mutex mtx;

void func() {
    std::scoped_lock lock(mtx);  // same idea as lock_guard
    // ...
}

Example 4: conditional locking

std::mutex m1, m2;

void func(bool needBoth) {
    if (needBoth) {
        std::scoped_lock lock(m1, m2);
        // ...
    } else {
        std::scoped_lock lock(m1);
        // ...
    }
}

lock_guard vs scoped_lock

C/C++ example:

// lock_guard: single mutex
std::lock_guard<std::mutex> lock(mtx);

// scoped_lock: multiple mutexes
std::scoped_lock lock(m1, m2, m3);

// scoped_lock is more general

Common pitfalls

Pitfall 1: deadlock

C/C++ example:

// ❌ manual locking (can deadlock)
m1.lock();
m2.lock();

// ✅ scoped_lock
std::scoped_lock lock(m1, m2);

Pitfall 2: ordering

// Thread 1
std::scoped_lock lock(m1, m2);

// Thread 2
std::scoped_lock lock(m2, m1);  // OK: deadlock avoided

Pitfall 3: exception safety

std::scoped_lock lock(m1, m2);
process();  // unlocks automatically even if an exception is thrown

Pitfall 4: not movable

C/C++ example:

std::scoped_lock lock(mtx);

// ❌ cannot move
// auto lock2 = std::move(lock);

// use unique_lock when you need move semantics
std::unique_lock<std::mutex> ulock(mtx);
auto ulock2 = std::move(ulock);  // OK

C++11 alternative

Example implementation of func:

// C++11: std::lock + lock_guard
std::mutex m1, m2;

void func() {
    std::lock(m1, m2);
    std::lock_guard<std::mutex> lock1(m1, std::adopt_lock);
    std::lock_guard<std::mutex> lock2(m2, std::adopt_lock);
}

// C++17: scoped_lock (simpler)
void func() {
    std::scoped_lock lock(m1, m2);
}

Production patterns

Pattern 1: safe swap

template<typename T>
class ThreadSafeContainer {
    mutable std::mutex mtx_;
    T data_;
    
public:
    void swap(ThreadSafeContainer& other) {
        if (this == &other) return;
        
        // deadlock avoidance
        std::scoped_lock lock(mtx_, other.mtx_);
        std::swap(data_, other.data_);
    }
    
    T get() const {
        std::scoped_lock lock(mtx_);
        return data_;
    }
};

Pattern 2: multi-resource update

class ResourceManager {
    std::mutex cpuMtx_, memMtx_, diskMtx_;
    int cpuUsage_, memUsage_, diskUsage_;
    
public:
    void updateAll(int cpu, int mem, int disk) {
        std::scoped_lock lock(cpuMtx_, memMtx_, diskMtx_);
        cpuUsage_ = cpu;
        memUsage_ = mem;
        diskUsage_ = disk;
    }
    
    void updateCpu(int cpu) {
        std::scoped_lock lock(cpuMtx_);
        cpuUsage_ = cpu;
    }
};

Pattern 3: layered locking

class BankSystem {
    std::mutex accountsMtx_;
    std::mutex transactionsMtx_;
    std::mutex auditMtx_;
    
public:
    void transfer(int from, int to, double amount) {
        // lock accounts and transactions
        std::scoped_lock lock(accountsMtx_, transactionsMtx_);
        // perform transfer
    }
    
    void audit() {
        // lock all resources
        std::scoped_lock lock(accountsMtx_, transactionsMtx_, auditMtx_);
        // run audit
    }
};

lock_guard vs scoped_lock vs unique_lock (at a glance)

All three are RAII mutex locks, but roles and costs differ.

lock_guardscoped_lockunique_lock
C++ versionC++11C++17C++11
Number of mutexes11 or more (variadic)1
Multi-mutex std::lockmanual compositionbuilt in (std::lock)std::unique_lock + std::lock pattern
Manual unlock / defer_locknonoyes (defer_lock, try_to_lock, etc.)
condition_variablelimited with mutex aloneusually use unique_lock with CVtypical pairing
Movenonoyes

Choosing

  • Single mutex, lock covers the whole blocklock_guard or scoped_lock with one argument. Some teams standardize on scoped_lock only.
  • Always lock two or more mutexes togetherscoped_lock is simplest and bundles the deadlock-avoidance algorithm.
  • Unlock mid-scope, relock, wait on condition variables, timed tryunique_lock is required.

Example implementation of wait_ready:

std::mutex m;
std::condition_variable cv;
bool ready = false;

void wait_ready() {
    std::unique_lock<std::mutex> lk(m);  // cannot replace with scoped_lock
    cv.wait(lk, [] { return ready; });
}

Deadlock avoidance: when lock ordering alone is not enough

scoped_lock applies std::lock-style ordering for multiple mutexes taken in the same call. The following situations remain risky:

  1. One thread uses scoped_lock(m1,m2) while another holds m1 and then blocks waiting for m2 in a mismatched design.
  2. Mixing code that reads shared data without a lock with code that writes under a lock.
  3. Nested locking through callbacks or virtual calls on a non-reentrant mutex when another lock is already held.

In production, assigning global order numbers to resources and always locking lower-numbered mutexes first, together with scoped_lock, makes reasoning and review easier. Even though scoped_lock already orders acquisition, explicit ordering rules help lock paths stand out in code review.

Practical checklist when locking multiple mutexes

  • If the same two objects can be locked in different orders, both scoped_lock(a,b) and scoped_lock(b,a) are safe, but verify that no path locks only a subset in a conflicting way.
  • When mixing std::shared_mutex with ordinary mutex, document read/write rules. scoped_lock can lock several at once as long as each type is BasicLockable.
  • Minimize hold time. Holding a broad scoped_lock across I/O or network calls hurts throughput.

Extra example: updating two adjacent nodes

When updating edge (u, v) in a graph and both vertex mutexes must be held, lock by vertex id order or use scoped_lock(mtx[u], mtx[v]).

struct Vertex {
    std::mutex mtx;
    int value{};
};

std::vector<Vertex> graph;

void add_edge_value(size_t u, size_t v, int delta) {
    if (u == v) return;
    // per-vertex mutexes — order varies; still avoids deadlock
    std::scoped_lock lk(graph[u].mtx, graph[v].mtx);
    graph[u].value += delta;
    graph[v].value += delta;
}

Performance: what actually costs

  • Single mutex, moderate contention: lock_guard and scoped_lock(mtx) are roughly the same cost (the mutex dominates).
  • Multiple mutexes: std::lock may try_lock, unlock, and retry, so the fast path (all locks on first try) can be slightly better than the worst case. Treat the difference as the price of no deadlocks.
  • Benchmarking: comparing nanoseconds in isolation matters less than end-to-end latency under real critical-section length and thread count.

In short: simplest option for a single lock, scoped_lock for multiple locks, unique_lock for condition variables and flexible locking.

FAQ

Q1: What is scoped_lock?

A: A RAII lock introduced in C++17 that locks multiple mutexes at once. It helps prevent deadlocks and unlocks automatically.

Example implementation of func:

std::mutex m1, m2;

void func() {
    std::scoped_lock lock(m1, m2);  // deadlock avoidance
    // automatic unlock
}

Q2: How does it prevent deadlock?

A: It uses the std::lock() deadlock-avoidance algorithm internally to acquire multiple mutexes safely.

// Thread 1
std::scoped_lock lock(m1, m2);

// Thread 2
std::scoped_lock lock(m2, m1);  // OK: deadlock avoided

Mechanism: a try-lock-based approach locks all mutexes without deadlock.

Q3: How is it different from lock_guard?

A:

  • lock_guard: single mutex only
  • scoped_lock: single or multiple mutexes

C/C++ example:

// lock_guard: single only
std::lock_guard<std::mutex> lock(mtx);

// scoped_lock: single or multiple
std::scoped_lock lock1(mtx);        // single
std::scoped_lock lock2(m1, m2, m3); // multiple

Recommendation: on C++17 and later, prefer scoped_lock.

Q4: Is it movable?

A: No. scoped_lock is neither copyable nor movable. Use unique_lock if you need move semantics.

C/C++ example:

std::scoped_lock lock(mtx);
// auto lock2 = std::move(lock);  // error

// use unique_lock
std::unique_lock<std::mutex> ulock(mtx);
auto ulock2 = std::move(ulock);  // OK

Q5: What about performance?

A: Same as lock_guard for a single mutex. With multiple mutexes there is a small overhead from the avoidance algorithm; for safety it is usually negligible.

// single: same cost as lock_guard
std::scoped_lock lock(mtx);

// multiple: small overhead (deadlock avoidance)
std::scoped_lock lock(m1, m2, m3);

Q6: What about C++11/14?

A: Combine std::lock() with lock_guard.

Example implementation of func:

// C++11/14
std::mutex m1, m2;

void func() {
    std::lock(m1, m2);  // deadlock avoidance
    std::lock_guard<std::mutex> lock1(m1, std::adopt_lock);
    std::lock_guard<std::mutex> lock2(m2, std::adopt_lock);
}

// C++17: much simpler
void func() {
    std::scoped_lock lock(m1, m2);
}

Q7: Can I conditionally lock multiple mutexes?

A: Yes, but each branch needs its own scoped_lock.

void func(bool needBoth) {
    if (needBoth) {
        std::scoped_lock lock(m1, m2);
        // lock both
    } else {
        std::scoped_lock lock(m1);
        // lock m1 only
    }
}

Q8: Learning resources for scoped_lock?

A:

Related posts: Mutex & lock_guard, shared_mutex, data races & mutexes, thread basics.

One-line summary: std::scoped_lock is a C++17 RAII lock that safely locks multiple mutexes without deadlock.


Posts that connect to this topic:

  • C++ data race
  • C++ any
  • Modern C++ cheat sheet
  • C++ CTAD
  • C++ string vs string_view