/

C++ mutex lock

안녕하세요, 코딩샐러드 입니다.

C++ 뿐만 아니라 여러 프로그래밍 언어에서 mutex_lock은 멀티스레드 프로그래밍을 할 때 매우 중요합니다. 2개 이상의 스레드에서 동시에 값을 변경하게 되는 경우 의도하지 않은 동작을 유발할 수 있기 때문 입니다.

이번에는 C++ 에서 제공하는 mutex_lock에 대해서 알아보도록 하겠습니다.

그리고 어떤 방식으로 사용하는 것이 좋을까에 대한 제 개인적인 생각을 써보려고 합니다.

기본적인 lock / unlock 그리고 문제점

아래 예제는 2개의 스레드를 생성하여 g_num이라는 글로벌 변수를 증가시키는 예제 입니다.

이때 std::mutex g_num_mutex 라는 변수를 선언하는데요, 이 변수가 뮤텍스 락에 사용하는 변수 입니다. lock()/ unlock() 함수를 사용하는데 사용 됩니다.

#include <iostream>
#include <chrono> 
#include <thread> // thread 헤더 추가
#include <mutex>  // mutex 헤더 추가

int g_num = 0;          // g_num_mutex에 의해 보호되는 global 변수
std::mutex g_num_mutex; // mutex lock/unlock에 사용되는 변수

void slow_increment(int id) 
{
    for (int i = 0; i < 3; ++i) 수
        g_num_mutex.lock(); 
        ++g_num;
        // note, that the mutex also syncronizes the output
        std::cout << "id: " << id << ", g_num: " << g_num << '\n';
        g_num_mutex.unlock();

        std::this_thread::sleep_for(std::chrono::milliseconds(234));
    }
}

int main()
{
    std::thread t1{slow_increment, 0}; // id 0 생성
    std::thread t2{slow_increment, 1}; // id 1 생성
    t1.join(); // 종료될 때 까지 기다림
    t2.join(); // 종료될 때 까지 기다림
}

실행결과

id: 0, g_num: 1
id: 1, g_num: 2
id: 0, g_num: 3
id: 1, g_num: 4
id: 1, g_num: 5
id: 0, g_num: 6

유의할 점은 바로 unlock을 꼭 해줘야 한다는 것 입니다.

코드를 약간 수정하여, void slow_increment(int id) 함수의 for 반복문 안에서 g_num이 5인 경우에 continue를 사용하여 unlock 되지 않게 해보겠습니다.

void slow_increment(int id) 
{
    for (int i = 0; i < 3; ++i) {
        g_num_mutex.lock(); 
        ++g_num;
        // note, that the mutex also syncronizes the output
        std::cout << "id: " << id << ", g_num: " << g_num << '\n';
        if(g_num == 5){
            std::cout << "skip unlock - will be dead lock" << std::endl;
            continue;
        }
        g_num_mutex.unlock();

        std::this_thread::sleep_for(std::chrono::milliseconds(234));
    }
}

이 경우에는 g_num이 5인 경우에는 unlock을 하지 않아 g_num_mutex에 lock이 걸려있는 상태로 데드락에 빠지게 될 것 입니다. 직접 실행해볼게요.

이 상태에서는 아무리 기다려 보아도 종료되지 않는 것을 확인할 수 있습니다.

id: 0, g_num: 1
id: 1, g_num: 2
id: 1, g_num: 3
id: 0, g_num: 4
id: 1, g_num: 5
skip unlock - will be dead lock

지금은 의도적으로 unlock이 안되어 교착상태에 빠지게 만든 상황 이지만, 프로그래머가 의도치 않게 lock을 걸고 unlock을 빠뜨리는 경우에는 이런 문제가 빈번하게 발생할 수 있습니다.

그래서 저는 mutex의 lock과 unlock은 사용하지 않는 것을 추천 드립니다.

lock_guard 사용하기

위에 void slow_increment(int id) 함수안에서 lock / unlock을 거는 부분을 모두 삭제하고, lock_guard로 대체해보겠습니다.

void slow_increment(int id) 
{
    const std::lock_guard<std::mutex> lock(g_num_mutex); // lock_guard로 대체

    for (int i = 0; i < 3; ++i) {
        //g_num_mutex.lock();  // lock은 사용하지 않는다.
        ++g_num;
        // note, that the mutex also syncronizes the output
        std::cout << "id: " << id << ", g_num: " << g_num << '\n';
        if(g_num == 5){
            std::cout << "skip - will be dead lock??" << std::endl;
            continue;
        }
        //g_num_mutex.unlock(); // unlock은 사용하지 않는다.

        std::this_thread::sleep_for(std::chrono::milliseconds(234));
    }
        // lock_guard에 의해서 스코프를 벗어나면 자동으로 lock이 해제 됩니다.
}

실행결과

id: 0, g_num: 1
id: 0, g_num: 2
id: 0, g_num: 3
id: 1, g_num: 4
id: 1, g_num: 5
skip - will be dead lock??
id: 1, g_num: 6

실행 결과의 id값을 확인해보면 다른 결과가 나온 것을 확인할 수 있습니다.

lock_guard가 선언된 block안의 모든 동작이 끝날때 까지 lock이 걸리게 되는데요,

이 경우에는 id = 0 인 스레드가 먼저 lock_guard로 slow_increment함수를 독점하여 사용합니다.

그 후에 id = 1 인 스레드가 lock_guard로 slow_increment 함수를 독점하여 사용하게 됩니다.

그리고 lock / unlock을 따로 하지 않아도, 종료되는 시점에(unlock을 해야 하는 시점) 자동으로 lock을 해제하게 됩니다.

lock_guard 함수의 설명을 확인해보면 아래와 같은 설명을 찾을 수 있습니다.

The class lock_guard is a mutex wrapper that provides a convenient RAII-style mechanism 
for owning a mutex for the duration of a scoped block.

When a lock_guard object is created, it attempts to take ownership of the mutex it is given. 
When control leaves the scope in which the lock_guard object was created, 
the lock_guard is destructed and the mutex is released.

The lock_guard class is non-copyable.

scoped block안에서 mutex 소유 하는 RAII-sytle 기법을 사용하는 것이라고 설명 되어 있습니다.

RAII 란?

RAII는 Resource Acquisition Is Initialization 의 약자 입니다.
사용 전에 획득해야 하는 리소스의 수명 주기(할당된 힙 메모리, 실행 스레드, 오픈 소켓, 오픈 파일, 잠금된 뮤텍스, 디스크 공간, 데이터베이스 연결—제한된 공급에 존재하는 모든 것)를 개체의 수명에 바인딩하는 C++ 프로그래밍 기술 입니다.

lock_guard가 생성이 되면 mutex 제어권을 가지고, lock_guard가 생성된 스코프를 벗어나면 제어권을 자동으로 해제 합니다.

lock / unlock을 신경 쓰지 않고, 자동으로 해제해주는 lock_guard를 사용하는 것을 추천 드립니다.

cppreference에도 lock을 직접 사용하는 것은 나쁜 예시라고 되어 있습니다.

RAII sytle의 mutex_lock에는 lock_guard / unique_lock / shared_lock / scoped_lock 등이 존재합니다.

여러개의 스레드를 사용하는 경우에는 꼭 RAII-style의 lock을 사용 하세요!

+ Recent posts