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을 사용 하세요!
'C++' 카테고리의 다른 글
C++ 함수 포인터 (0) | 2022.12.14 |
---|---|
C++ 기본생성자(default constructor) 와 생성자 오버로딩 (0) | 2022.11.20 |
C++ 템플릿이란? Template은 generic을 위함 (0) | 2022.10.17 |
C++ 싱글턴(singleton) 디자인 패턴 구현 (0) | 2022.10.16 |
클래스 생성자와 소멸자 (0) | 2022.10.15 |