[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:
- Lock the first mutex.
- Try to lock the rest with
try_lock(). - On failure, unlock all and retry.
- 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_guard | scoped_lock | unique_lock | |
|---|---|---|---|
| C++ version | C++11 | C++17 | C++11 |
| Number of mutexes | 1 | 1 or more (variadic) | 1 |
Multi-mutex std::lock | manual composition | built in (std::lock) | std::unique_lock + std::lock pattern |
Manual unlock / defer_lock | no | no | yes (defer_lock, try_to_lock, etc.) |
condition_variable | limited with mutex alone | usually use unique_lock with CV | typical pairing |
| Move | no | no | yes |
Choosing
- Single mutex, lock covers the whole block →
lock_guardorscoped_lockwith one argument. Some teams standardize onscoped_lockonly. - Always lock two or more mutexes together →
scoped_lockis simplest and bundles the deadlock-avoidance algorithm. - Unlock mid-scope, relock, wait on condition variables, timed try →
unique_lockis 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:
- One thread uses
scoped_lock(m1,m2)while another holdsm1and then blocks waiting form2in a mismatched design. - Mixing code that reads shared data without a lock with code that writes under a lock.
- 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)andscoped_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_lockcan lock several at once as long as each type is BasicLockable. - Minimize hold time. Holding a broad
scoped_lockacross 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_guardandscoped_lock(mtx)are roughly the same cost (the mutex dominates). - Multiple mutexes:
std::lockmay 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:
- C++ Concurrency in Action (2nd ed.) by Anthony Williams
- C++17 — The Complete Guide by Nicolai Josuttis
- cppreference.com — std::scoped_lock
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.
Related reading (internal links)
Posts that connect to this topic:
- C++ Mutex & Lock guide
- C++ shared_mutex guide
- C++ data race & mutex vs atomic
- C++ std::thread basics
More related posts
- C++ data race
- C++ any
- Modern C++ cheat sheet
- C++ CTAD
- C++ string vs string_view