본문 바로가기

진리는어디에/C++

[C++] 스레드 동기화 - lock_guard

들어가며

이전 포스트 '스레드 동기화 - mutex'에서는 C++ 표준 라이브러리에서 제공하는 std::mutex 객체의 lock, unlock 함수를 직접 호출하여 크리티컬 섹션에 진입할 수 있는 스레드를 직접 컨트롤 했었다. 하지만 이런 방법은 앞으로 살펴 볼 '실수'를 유발할 수 있고, 이 실수들은 데드락을 발생 시켜 프로그램을 영원히 블록 시킬수 있다.

이번 포스트에서는 std::mutex의 멤버 함수를 직접 호출할 때 야기할 수 있는 '실수'들을 살펴 보고, std::lock_guard를 사용했을 때의 이점에 대해 살펴 보도록 하겠다.

std::mutex의 문제

위에서 std::mutex의 lock과 unlock 멤버 함수를 직접 호출하는 경우 프로그래머의 '실수'로 인해 프로그램이 데드락 상태에 빠질 수 있다고 이야기 했다. 아래 예제는 정상적인 멀티 스레드 프로그램이지만 약간의 수정(실수 유발)으로 프로그램이 데드락에 빠질 수 있는 상태에 대해 살펴 보도록 하자.

#include <iostream>
#include <thread>
#include <mutex>
#include <exception>

std::mutex m;

void bar()
{
    m.lock();
    std::cout << "access shared data" << std::endl;
    m.unlock();
}

void foo()
{
    try
    {
        bar();
    }
    catch (...)
    {
        std::cout << "exception dected" << std::endl;
    }
}

int main()
{
    std::thread t1(foo);
    std::thread t2(foo);

    t1.join();
    t2.join();

    return 0;
}

lock을 호출 했지만 unlock을 누락한 경우

void bar()
{
    m.lock();
    std::cout << "access shared data" << std::endl;
    // m.unlock();  // 깜빡하고 unlock을 빼먹음
}

만일 위 예제 처럼 lock 멤버 함수를 호출 후 어떠한 이유(주로 복잡한 코드 가독성)로 인해 깜빡하고 unlock을 호출을 빼먹는 경우, 스레드가 크리티컬 섹션에 일단 진입하고 나면 다른 스레드들은 mutex를 획득하지 못해 데드락 상태에 빠지게 된다.

예외가 발생하는 경우

void bar()
{
    m.lock();
    std::cout << "access shared data" << std::endl;
    throw std::exception();
    m.unlock();
}

앞선 예제와 달리 lock 이후 unlock을 호출했지만 그 사이에 예외가 발생하는 경우, 프로그램은 unlock까지 진행되지 못하고 도중에 빠져나가 foo 함수의 catch에 걸리게 된다. 이 경우 역시 unlock이 호출 되지 못했으므로 다른 스레드들은 크리티컬 섹션에 진입하지 못하고 프로그램은 데드락 상태에 빠지게 된다.

std::lock_guard

이야기가 길어 졌지만 위의 내용을 요약하면 다음과 같다.

  • lock을 호출 후 실수로 unlock 호출을 하지 않을 수 있다.
  • 예외가 발생하면 unlock이 호출 되지 않는다.

그래서 C++에서는 사용자가 직접 lock/unlock을 호출하는 대신 lock_guard를 이용해 lock 객체를 사용하는 것을 권장한다. lock_guard란 생성자에서 lock 객체의 lock(), 소멸자에서 unlock() 멤버 함수를 호출해주는 간단한 도구다(RAII).

아래의 예제를 살펴 보자. lock/unlock을 직접 호출하는 대신 lock_guard를 사용하고 있다.

/* lock/unlock 직접 호출 대신 lock_guard 사용 */

void bar()
{
    std::lock_guard<std::mutex> lo(m);
    
    // m.lock();
    std::cout << "access shared data" << std::endl;
    throw std::exception();
    // m.unlock();
}

위 코드의 5라인에서 lock_guard 지역 변수가 생성 되면, 생성자에서 mutex lock 객체의 lock 멤버 함수를 호출하고 함수가 종료 되어 lock_guard 지역 변수가 파괴 될 때 소멸자에서 unlock을 자동으로 호출해 준다.

위 예제 처럼 lock_guard를 사용하게 되면 실수로 unlock을 누락할 일도 없어질 뿐더러, 심지어 프로그램 실행 도중 예외가 발생하더라도 C++은 예외 발생 시에도 지역 변수는 안전하게 파괴하므로, 지역 변수의 소멸자 호출을 통해 unlock을 보장한다.

요약

std::mutex를 사용할 때는 std::lock_guard를 이용해 사용하도록 한다.

부록 1. 같이 보면 좋은 글

유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!