728x90

저번 포스트에서 멀티 쓰레드 프로그래밍시 race condition에 의해 공유 자원을 사용하는 critical section에 자원을 동시에 접근했을때 문제가 생기는것을 알아보았다.

이런 상황을 방지할 때 사용할 수 있는 mutex라는것에 대해서도 알아보았다.

 

이번 포스트에서는 아래와 같은 것에 대해 공부해보고자 한다.

 

1.  mutex.lock()을 걸어두고, mutex.unlock()을 해주지 않았을때 어떠한 문제가 발생하는지

2. 위의 문제를 방지하기 위한 유용한 것들

 

mutex unlock을 해주지 않았을때 발생하는 문제점

저번 포스트에서 mutex를 공용 화장실에 자물쇠에 비유했었다.

동일하게 mutex를 자물쇠라고 생각했을때, lock()을 한 상황은 어떤 한 사람이 화장실에 들어가서 문을 잠궜다고 볼 수 있다.

이때, 잠근 문을 열지 않고 다른 곳(예를들어 창문같은)으로 나갔다고 생각해보자.

뒤이어 오는 사람들은 화장실 문이 잠겨있으니  자물쇠를 부수지 않고서는 화장실을 사용할 수 없다.

 

멀티 쓰레드 프로그래밍에서도 위와 동일하다.

mutex.lock()을 걸어놓은 스레드에서 unlock()을 하지 않았을 경우, 다른 스레드에서 만약 이부분에 접근하게 된다면

무한정 대기하게 된다.

이러한 경우를 Dead lock(데드락) 상황이라고 한다.

우리는 멀티 쓰레드 프로그래밍을 할때 데드락 상황이 발생하지 않도록 많은 상황을 고려하여 코드를 작성해야만 한다.

 

 

데드락 상황을 방지하기 위한 RAII

mutex.unlock()을 해주지 않아 발생하는 데드락에 대해서는 해결할 수 있는 간단한 방법이 있다.

C++ 프로그래밍 기술중에 RAII(Resource Acquisition Is Initialization)이라는 것이 있다.

우리가 사용하는 resource를 클래스로 캡슐화해서 생성자에서 할당받도록 하는것이다.

이렇게 됐을때, 스코프를 벗어나게되면 자동으로 소멸자가 호출되면서 해당 리소스를 반납하게 된다.

 

mutex를 생성자에서 받고 소멸자에서 unlock하도록 도와주는 클래스가 있다.

바로 std::lock_guard이다.

std::lock_guard

#include <iostream>
#include <thread>
#include <mutex>
 
int g_i = 0;
std::mutex g_i_mutex;  // protects g_i
 
void safe_increment()
{
    const std::lock_guard<std::mutex> lock(g_i_mutex);
    ++g_i;
 
    std::cout << "g_i: " << g_i << "; in thread #"
              << std::this_thread::get_id() << '\n';
 
    // g_i_mutex is automatically released when lock
    // goes out of scope
}
 
int main()
{
    std::cout << "g_i: " << g_i << "; in main()\n";
 
    std::thread t1(safe_increment);
    std::thread t2(safe_increment);
 
    t1.join();
    t2.join();
 
    std::cout << "g_i: " << g_i << "; in main()\n";
}

lock_guard는 mutex 헤더에 포함되어 있다.

사용하는 방법은 lock_guard<mutex> 변수명(mutex변수명) 이런식으로 lock을 걸 위치에 객체를 생성하면 된다.

생성한 스코프에서 벗어나면 자동으로 unlock이 호출될 것이다.

결과는 아래와 같이 출력될 것이다.

g_i: 0; in main()
g_i: 1; in thread #140487981209344
g_i: 2; in thread #140487972816640
g_i: 2; in main()

lock_guard와 비슷하지만 기능이 더해진 클래스도 있다.

바로 unique_lock이다.

std::unique_lock

unique_lock도 기본적으로 lock_guard와 동일하게 생성자가 호출될때 lock, 소멸자 호출시 unlock되도록 되어있다.

unique_lock을 생성할때 아래와 같은 옵션을 넣어 줄 수 있다.

 

1. std::defer_lock

둘 이상의 뮤텍스를 사용할때, 데드락이 발생할 수도 있는 상황에서 주로 사용된다.

생성될때 mutex lock을 걸지않고, 따로 lock()함수를 이용할때 잠금이 걸린다. 

2. std::adopt_lock

생성시에 lock이 걸리지 않는다.

이미 mutex lock이 걸려있다고 가정하고, 이후에 unlock을 하기 위해 사용된다.

3. std::try_to_lock

생성시에 lock이 걸리지 않는다.

내부적으로 try_lock()을 호출하게 되는데, try_lock() 함수의 동작은 mutex lock()을 시도하는데, 이미 lock()이 걸려있는 상황이면 false가 리턴된다. owns_lock()함수를 통해 잠금이 가능한 상황인지 파악할 수 있다.

 

#include <mutex>
#include <thread>
#include <iostream>
 
struct Box
{
    explicit Box(int num) : num_things{num} {}
 
    int num_things;
    std::mutex m;
};
 
void transfer(Box &from, Box &to, int num)
{
    // don't actually take the locks yet
    std::unique_lock lock1{from.m, std::defer_lock};
    std::unique_lock lock2{to.m, std::defer_lock};
 
    // lock both unique_locks without deadlock
    std::lock(lock1, lock2);
 
    from.num_things -= num;
    to.num_things += num;
 
    // 'from.m' and 'to.m' mutexes unlocked in 'unique_lock' dtors
}
 
int main()
{
    Box acc1{100};
    Box acc2{50};
 
    std::thread t1{transfer, std::ref(acc1), std::ref(acc2), 10};
    std::thread t2{transfer, std::ref(acc2), std::ref(acc1), 5};
 
    t1.join();
    t2.join();
 
    std::cout << "acc1: " << acc1.num_things << "\n"
                 "acc2: " << acc2.num_things << '\n';
}

계좌에서 송금하는 상황이 작성된 코드다.

각각의 Box 객체가 mutex를 가지고 있다.

 

transfor()함수를 확인해보면,

    // don't actually take the locks yet
    std::unique_lock lock1{from.m, std::defer_lock};
    std::unique_lock lock2{to.m, std::defer_lock};
 
    // lock both unique_locks without deadlock
    std::lock(lock1, lock2);

위와 같이 작성되어 있다.

mutex가 2개 이상일때, 서로 엇갈리게 lock을 하는 상황은 굉장히 위험하다.

아래의 그림을 보자

thread 1이 acc1의 mutex를 잠근 상황에서 acc2의 mutex를 잠그기 전에

thread 2에서 acc2의 mutex를 잠그고 acc1의 mutex을 잠그려고 시도하게 되면 문제가 발생한다.

thread1에서는 acc2의 mutex lock을 하기 위해 잠금이 해제될때까지 무한 대기를 하는 상황이고,

thread2에서는 acc1의 mutex lock을 하기 위해 잠금이 해제될때까지 무한 대기를 하게 된다.

이러한 상황 또한 공용자원에 접근하지 못하고 프로그램이 무한정 대기상태에 들어가는 데드락 상황이라고 할 수 있다.

 

이런 상황일때 위 처럼 unique_lock으로 std::defer_lock 옵션을 준 후에

std::lock 함수를 호출하여 lock을 거는 순서를 정해주면 데드락 상황을 예방할 수 있다.

 

위 코드의 결과는 아래와 같다.

acc1: 95
acc2: 55

 

다음 포스트에서는 mutex를 사용하여 멀티 스레드 프로그래밍시 도움되는 condition_variable에 대해 공부해보고 작성해도록 하려고 한다.

 

 

'Development > C++' 카테고리의 다른 글

[C++] Mutex  (0) 2022.10.08
[C++] Process와 thread  (0) 2022.10.05
복사했습니다!