icon안동민 개발노트

뮤텍스와 락


뮤텍스 (Mutex)의 개념

 멀티스레드 프로그래밍에서 공유 자원에 대한 동시 접근을 제어하는 것은 매우 중요합니다. 이를 위해 C++은 뮤텍스(mutex)와 락(lock)을 제공합니다.

 뮤텍스(Mutual Exclusion의 줄임말)는 여러 스레드가 공유 자원에 동시에 접근하는 것을 방지하는 동기화 객체입니다. C++에서는 <mutex> 헤더에 정의된 std::mutex 클래스를 사용하여 뮤텍스를 구현합니다.

기본 사용법
#include <iostream>
#include <thread>
#include <mutex>
 
std::mutex mtx;
int shared_value = 0;
 
void increment() {
    mtx.lock();
    ++shared_value;
    mtx.unlock();
}
 
int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    
    t1.join();
    t2.join();
    
    std::cout << "Shared value: " << shared_value << std::endl;
    return 0;
}

 mtx.lock()은 뮤텍스를 잠그고, mtx.unlock()은 뮤텍스를 해제합니다.

락 가드 (Lock Guard)

 std::lock_guard는 RAII(Resource Acquisition Is Initialization) 원칙을 따르는 락 객체로, 생성 시 뮤텍스를 자동으로 잠그고 소멸 시 자동으로 해제합니다.

#include <iostream>
#include <thread>
#include <mutex>
 
std::mutex mtx;
int shared_value = 0;
 
void increment() {
    std::lock_guard<std::mutex> lock(mtx);
    ++shared_value;
}
 
int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    
    t1.join();
    t2.join();
    
    std::cout << "Shared value: " << shared_value << std::endl;
    return 0;
}

유니크 락 (Unique Lock)

 std::unique_lockstd::lock_guard보다 더 유연한 락 객체입니다. 락의 소유권을 이전하거나 조건 변수와 함께 사용할 수 있습니다.

#include <iostream>
#include <thread>
#include <mutex>
 
std::mutex mtx;
int shared_value = 0;
 
void increment() {
    std::unique_lock<std::mutex> lock(mtx);
    ++shared_value;
    lock.unlock();  // 명시적으로 락 해제 가능
    
    // 다른 작업 수행
    
    lock.lock();  // 다시 락 획득
    ++shared_value;
}
 
int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    
    t1.join();
    t2.join();
    
    std::cout << "Shared value: " << shared_value << std::endl;
    return 0;
}

데드락 (Deadlock)

 데드락은 두 개 이상의 스레드가 서로 상대방이 가진 리소스를 기다리며 무한히 대기하는 상황입니다.

#include <iostream>
#include <thread>
#include <mutex>
 
std::mutex mtx1, mtx2;
 
void thread_1() {
    std::lock_guard<std::mutex> lock1(mtx1);
    std::this_thread::sleep_for(std::chrono::milliseconds(1));
    std::lock_guard<std::mutex> lock2(mtx2);
    std::cout << "Thread 1 acquired both locks" << std::endl;
}
 
void thread_2() {
    std::lock_guard<std::mutex> lock2(mtx2);
    std::this_thread::sleep_for(std::chrono::milliseconds(1));
    std::lock_guard<std::mutex> lock1(mtx1);
    std::cout << "Thread 2 acquired both locks" << std::endl;
}
 
int main() {
    std::thread t1(thread_1);
    std::thread t2(thread_2);
    
    t1.join();
    t2.join();
    
    return 0;
}

std::lock과 std::scoped_lock

 C++ 17부터는 std::scoped_lock을 사용하여 여러 뮤텍스를 한 번에 안전하게 잠글 수 있습니다.

#include <iostream>
#include <thread>
#include <mutex>
 
std::mutex mtx1, mtx2;
 
void safe_thread() {
    std::scoped_lock lock(mtx1, mtx2);
    std::cout << "Thread acquired both locks" << std::endl;
}
 
int main() {
    std::thread t1(safe_thread);
    std::thread t2(safe_thread);
    
    t1.join();
    t2.join();
    
    return 0;
}

재귀적 뮤텍스 (Recursive Mutex)

 std::recursive_mutex는 같은 스레드에서 여러 번 잠글 수 있는 뮤텍스입니다.

#include <iostream>
#include <thread>
#include <mutex>
 
std::recursive_mutex rmtx;
 
void recursive_function(int depth) {
    std::lock_guard<std::recursive_mutex> lock(rmtx);
    std::cout << "Recursion depth: " << depth << std::endl;
    
    if (depth > 0) {
        recursive_function(depth - 1);
    }
}
 
int main() {
    recursive_function(5);
    return 0;
}

연습 문제

  1. 생산자-소비자 문제를 구현하세요. 생산자 스레드는 데이터를 생성하여 버퍼에 넣고, 소비자 스레드는 버퍼에서 데이터를 꺼내 처리합니다. 뮤텍스와 조건 변수를 사용하여 동기화를 구현하세요.
  2. 읽기-쓰기 락을 구현하세요. 여러 스레드가 동시에 읽기 작업을 수행할 수 있지만, 쓰기 작업은 배타적으로 수행되어야 합니다.
  3. 데드락을 방지하기 위한 계층적 뮤텍스 잠금 전략을 구현하세요. 여러 개의 뮤텍스를 사용할 때 항상 정해진 순서대로 잠그도록 합니다.


참고 자료

  • C++ Concurrency in Action (2nd Edition) by Anthony Williams
  • Effective Modern C++ by Scott Meyers (Item 35-40)
  • C++ 표준 문서의 스레드와 동기화 관련 섹션
  • CppCon 발표 영상들 - 동시성 프로그래밍 관련 세션들
  • "The Art of Multiprocessor Programming" by Maurice Herlihy and Nir Shavit