[2026] C++ scoped_lock | 범위 락 가이드

[2026] C++ scoped_lock | 범위 락 가이드

이 글의 핵심

std::scoped_lock(C++17)은 std::lock 기반으로 여러 뮤텍스를 한 번에 잠그는 RAII 락입니다. lock_guard·unique_lock과의 차이, 데드락 회피, 다중 잠금 실전 패턴과 성능 관점까지 정리합니다.

scoped_lock이란?

std::scoped_lock (C++17)은 여러 뮤텍스를 한 번에 잠그고, 범위를 벗어날 때 자동으로 모두 해제하는 RAII 락입니다. lock_guard의 다중 뮤텍스 버전이며, 데드락 방지를 위해 항상 같은 순서로 잠급니다. 스레드 기초데이터 레이스·뮤텍스를 먼저 보면 좋습니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 코드를 직접 실행해보면서 동작을 확인해보세요.

#include <mutex>
std::mutex m1, m2;
void func() {
    std::scoped_lock lock(m1, m2);  // 데드락 방지
    // ...
}

왜 필요한가?:

  • 데드락 방지: 여러 뮤텍스를 안전하게 잠금
  • RAII: 자동 해제로 예외 안전
  • 간결성: C++11의 복잡한 코드를 단순화
  • 범용성: 단일 및 다중 뮤텍스 모두 지원 다음은 cpp를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 수동 잠금: 데드락 위험
std::mutex m1, m2;
void transfer1() {
    m1.lock();
    m2.lock();  // 데드락 가능!
    // ...
    m2.unlock();
    m1.unlock();
}
void transfer2() {
    m2.lock();
    m1.lock();  // 데드락 가능!
    // ...
    m1.unlock();
    m2.unlock();
}
// ✅ scoped_lock: 데드락 방지
void transfer1() {
    std::scoped_lock lock(m1, m2);  // 안전
    // ...
}
void transfer2() {
    std::scoped_lock lock(m2, m1);  // 안전 (순서 무관)
    // ...
}

scoped_lock의 동작 원리: scoped_lock은 내부적으로 std::lock()을 사용하여 데드락 회피 알고리즘으로 모든 뮤텍스를 잠급니다. 소멸자에서 자동으로 모든 뮤텍스를 해제합니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 개념적 구현
// 실행 예제
template<typename....Mutexes>
class scoped_lock {
    std::tuple<Mutexes&...> mutexes_;
    
public:
    scoped_lock(Mutexes&....mutexes) : mutexes_(mutexes...) {
        std::lock(mutexes...);  // 데드락 회피 알고리즘
    }
    
    ~scoped_lock() {
        // 역순으로 해제
        unlock_all(mutexes_);
    }
    
    // 복사/이동 불가
    scoped_lock(const scoped_lock&) = delete;
    scoped_lock& operator=(const scoped_lock&) = delete;
};

데드락 회피 알고리즘: std::lock()try-lock 기반 알고리즘을 사용하여 데드락을 방지합니다:

  1. 첫 번째 뮤텍스를 잠금
  2. 나머지 뮤텍스를 try_lock()으로 시도
  3. 실패 시 모두 해제하고 재시도
  4. 모두 성공할 때까지 반복 아래 코드는 cpp를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// std::lock의 개념적 동작
// 실행 예제
void lock(Mutex& m1, Mutex& m2) {
    while (true) {
        m1.lock();
        if (m2.try_lock()) {
            return;  // 성공
        }
        m1.unlock();  // 실패, 재시도
        
        std::this_thread::yield();
    }
}

기본 사용

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

std::mutex mtx;
void func() {
    std::scoped_lock lock(mtx);
    // 자동 잠금/해제
}

실전 예시

예시 1: 계좌 이체

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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

예시 2: 여러 자원

아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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

예시 3: 단일 뮤텍스

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

std::mutex mtx;
void func() {
    std::scoped_lock lock(mtx);  // lock_guard와 동일
    // ...
}

예시 4: 조건부 잠금

아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// lock_guard: 단일 뮤텍스
std::lock_guard<std::mutex> lock(mtx);
// scoped_lock: 여러 뮤텍스
std::scoped_lock lock(m1, m2, m3);
// scoped_lock이 더 범용적

자주 발생하는 문제

문제 1: 데드락

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ❌ 수동 잠금 (데드락 가능)
m1.lock();
m2.lock();
// ✅ scoped_lock
std::scoped_lock lock(m1, m2);

문제 2: 순서

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// Thread 1
std::scoped_lock lock(m1, m2);
// Thread 2
std::scoped_lock lock(m2, m1);  // OK: 데드락 방지

문제 3: 예외 안전성

std::scoped_lock lock(m1, m2);
process();  // 예외 발생해도 자동 unlock

문제 4: 이동 불가

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

std::scoped_lock lock(mtx);
// ❌ 이동 불가
// auto lock2 = std::move(lock);
// unique_lock 사용 필요
std::unique_lock<std::mutex> ulock(mtx);
auto ulock2 = std::move(ulock);  // OK

C++11 대안

아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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 (간단)
void func() {
    std::scoped_lock lock(m1, m2);
}

실무 패턴

패턴 1: 안전한 스왑

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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

패턴 2: 다중 자원 업데이트

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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;
    }
};

패턴 3: 계층적 잠금

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

class BankSystem {
    std::mutex accountsMtx_;
    std::mutex transactionsMtx_;
    std::mutex auditMtx_;
    
public:
    void transfer(int from, int to, double amount) {
        // 계좌와 트랜잭션 잠금
        std::scoped_lock lock(accountsMtx_, transactionsMtx_);
        // 계좌 이체
    }
    
    void audit() {
        // 모든 자원 잠금
        std::scoped_lock lock(accountsMtx_, transactionsMtx_, auditMtx_);
        // 감사 수행
    }
};

lock_guard vs scoped_lock vs unique_lock (한눈에 비교)

세 클래스 모두 RAII로 뮤텍스를 잠그지만, 역할과 비용이 다릅니다.

구분lock_guardscoped_lockunique_lock
C++ 버전C++11C++17C++11
뮤텍스 개수1개1개 이상 (가변 인자)1개
std::lock 다중 잠금직접 조합 필요내장 (std::lock 호출)std::unique_lock + std::lock 패턴
수동 unlock / defer_lock불가불가가능 (defer_lock, try_to_lock 등)
condition_variablemutex만으로는 제한적보통 CV와 함께 쓰기엔 unique_lock표준 조합
이동불가불가가능
선택 가이드
  • 단일 뮤텍스, 잠금 범위가 블록 전체lock_guard 또는 scoped_lock 한 개 인자. 스타일 통일을 위해 팀이 scoped_lock만 쓰기도 합니다.
  • 두 개 이상 뮤텍스를 항상 같이 잠가야 함scoped_lock이 가장 단순하고 데드락 회피 알고리즘이 한 번에 들어갑니다.
  • 중간에 잠금 해제, 재잠금, 조건 변수 대기, 타임아웃 시도unique_lock이 필수입니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
std::mutex m;
std::condition_variable cv;
bool ready = false;
void wait_ready() {
    std::unique_lock<std::mutex> lk(m);  // scoped_lock으로 대체 불가
    cv.wait(lk, [] { return ready; });
}

데드락 방지: 잠금 순서만으로 부족할 때

scoped_lock같은 호출에서 잡는 여러 뮤텍스에 대해 std::lock 기반으로 순서를 정리해 줍니다. 하지만 다음은 여전히 위험합니다.

  1. 한 스레드은 scoped_lock(m1,m2), 다른 스레드는 m1만 잡고 블로킹된 채 m2를 기다리는 식으로 설계가 어긋난 경우.
  2. 락 없이 공유 자료를 읽고, 다른 곳에서는 락을 잡고 쓰는 혼합 사용.
  3. 콜백·가상 함수 안에서 모르는 사이 다른 락을 잡는 비재진입(non-reentrant) 뮤텍스 중첩. 실무에서는 “전역 순서 번호”를 자원에 매기고, 항상 번호가 작은 뮤텍스부터 잠그는 규칙과 scoped_lock을 같이 쓰면 이해와 검증이 쉬워집니다. 이미 scoped_lock이 순서를 자동으로 맞춰 주더라도, 코드 리뷰 시 잠금 경로를 한눈에 보이게 하는 데 도움이 됩니다.

여러 뮤텍스를 동시에 잠글 때의 실무 체크

  • 같은 두 객체를 서로 다른 순서로 잠그는 코드가 있다면 scoped_lock(a,b)scoped_lock(b,a) 모두 안전하지만, 일부만 잠그는 경로가 섞이지 않았는지 확인하세요.
  • std::shared_mutex와 일반 mutex를 함께 쓸 때는 읽기/쓰기 규칙을 문서화하세요. scoped_lock은 타입이 달라도 여러 개를 한 번에 잠글 수 있습니다(각각이 BasicLockable이면 됨).
  • 락 보유 시간을 최소화하세요. scoped_lock으로 넓은 범위를 잡아 두고 I/O나 네트워크 호출을 하면 전체 처리량이 떨어집니다.

실전 예제 보강: 인접 노드 두 개를 동시에 갱신

그래프에서 엣지 (u, v)를 갱신할 때 두 정점의 내부 뮤텍스를 모두 잠가야 한다면, 정점 id 순으로만 잠그도록 하거나 scoped_lock(mtx[u], mtx[v])를 사용합니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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;
    // 정점별 뮤텍스 — 순서가 매번 바뀌어도 데드락 회피
    std::scoped_lock lk(graph[u].mtx, graph[v].mtx);
    graph[u].value += delta;
    graph[v].value += delta;
}

성능 비교: 무엇이 비싼가

  • 단일 mutex, 경쟁이 심하지 않을 때: lock_guardscoped_lock(mtx)동일한 수준의 비용(뮤텍스 잠금 자체가 지배적)입니다.
  • 다중 뮤텍스: std::lock은 필요 시 try_lock·언락·재시도를 하므로, 최선 경로(한 번에 모두 잠김)보다 약간 더 나쁠 수 있습니다. 그 대신 데드락 없음이라는 비용으로 보는 것이 맞습니다.
  • 벤치마크 시 주의: 나노초 단위로만 비교하기보다, 실제 임계 구역 길이·경쟁 스레드 수에서 전체 응답 시간을 재는 편이 의미 있습니다. 요약하면, 단일 락이면 세 타입 중 “가장 단순한 것”, 다중 락이면 scoped_lock, 조건 변수·유연한 잠금이면 unique_lock이 기본 선택입니다.

FAQ

Q1: scoped_lock은 무엇인가요?

A: C++17에서 도입된 여러 뮤텍스를 동시에 잠그는 RAII 락입니다. 데드락을 방지하고 자동으로 해제합니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

std::mutex m1, m2;
void func() {
    std::scoped_lock lock(m1, m2);  // 데드락 방지
    // 자동 해제
}

Q2: 데드락을 어떻게 방지하나요?

A: 내부적으로 std::lock()의 데드락 회피 알고리즘을 사용합니다. 여러 뮤텍스를 안전하게 잠급니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// Thread 1
std::scoped_lock lock(m1, m2);
// Thread 2
std::scoped_lock lock(m2, m1);  // OK: 데드락 방지

동작 원리: try_lock() 기반으로 모든 뮤텍스를 원자적으로 잠급니다.

Q3: lock_guard와 어떤 차이가 있나요?

A:

  • lock_guard: 단일 뮤텍스만 지원
  • scoped_lock: 단일 및 다중 뮤텍스 모두 지원 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// lock_guard: 단일만
std::lock_guard<std::mutex> lock(mtx);
// scoped_lock: 단일 및 다중
std::scoped_lock lock1(mtx);        // 단일
std::scoped_lock lock2(m1, m2, m3); // 다중

권장: C++17 이상에서는 scoped_lock 사용

Q4: 이동할 수 있나요?

A: 불가능합니다. scoped_lock은 복사와 이동이 모두 금지됩니다. 이동이 필요하면 unique_lock을 사용하세요. 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

std::scoped_lock lock(mtx);
// auto lock2 = std::move(lock);  // 에러
// unique_lock 사용
std::unique_lock<std::mutex> ulock(mtx);
auto ulock2 = std::move(ulock);  // OK

Q5: 성능은 어떤가요?

A: 단일 뮤텍스의 경우 lock_guard와 동일합니다. 다중 뮤텍스의 경우 데드락 회피 알고리즘으로 인한 약간의 오버헤드가 있지만, 안전성을 고려하면 무시할 수 있습니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// 단일: lock_guard와 동일한 성능
std::scoped_lock lock(mtx);
// 다중: 약간의 오버헤드 (데드락 회피)
std::scoped_lock lock(m1, m2, m3);

Q6: C++11/14에서는 어떻게 하나요?

A: std::lock()lock_guard를 조합하여 사용합니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// C++11/14
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: 훨씬 간단
void func() {
    std::scoped_lock lock(m1, m2);
}

Q7: 조건부로 여러 뮤텍스를 잠글 수 있나요?

A: 가능하지만, 각 경우마다 별도의 scoped_lock을 사용해야 합니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

void func(bool needBoth) {
    if (needBoth) {
        std::scoped_lock lock(m1, m2);
        // 둘 다 잠금
    } else {
        std::scoped_lock lock(m1);
        // m1만 잠금
    }
}

Q8: scoped_lock 학습 리소스는?

A:


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

관련 글

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3