C++11 상호 배제(Mutex)

|

Mutex 종류

C++ 표준에서는 Mutex 종류는 크게 2가지가 있습니다. 하나는 일반 Mutex이며, 다른 하나는 타임 아웃 Mutex 입니다.


일반 Mutex

일반 Mutex에는 std::mutex, std::recursive_mutext 두 종류가 있습니다. 두 클래스는 공통으로 다음과 같은 함수를 갖고 있습니다.

  • lock() : Thread에서 락 점유를 시도하며, 락을 획득할 때까지 무한정 대기함.
  • try_lock() : 락 점유를 시도하며, 성공시 true, 실패시 false를 바로 리턴함.
  • unlock() : 락을 해제함.

std::mutex는 중복해서 lock()을 호출할 경우 ‘데드락(Deadlock)’ 상황에 빠질 수 있습니다.

std::recursive_mutexlock()을 여러 번 호출해도 상관없지만, 대신 락을 해제할 때는 동일한 횟수만큼 unlock()을 호출해주어야 락이 해제됩니다.


타임 아웃 Mutex

타임 아웃 Mutex에는 std::timed_mutex, std::recursive_timed_mutex가 있습니다. 그리고 다음과 같은 함수들을 갖고 있습니다.

  • lock() : Thread에서 락 점유를 시도하며, 락을 획득할 때까지 무한정 대기함.
  • try_lock() : 락 점유를 시도하며, 성공시 true, 실패시 false를 바로 리턴함.
  • unlock() : 락을 해제함.
  • try_lock_for(rel_time) : 주어진 시간까지 락 점유를 시도. 시간은 상대 시간임. 성공시 true, 실패시 false를 바로 리턴.
  • try_lock_until(abs_time) : 주어진 시간까지 락 점유를 시도. 시간을 절대 시간임. 성공시 true, 실패시 false를 바로 리턴.


lock 클래스

Mutex를 좀 더 사용하기 쉽게 Wrapping한 클래스입니다. lock 클래스의 소멸자는 자동으로 내부의 Mutex를 해제해주기 때문에, 각 함수의 로컬 변수로 사용할 경우 함수를 빠져나오면서 자연스럽게 락이 해제됩니다. 즉, 락 해제에 신경을 덜 쓸 수 있습니다.

lock_guard는 생성자에서 락 점유를 시도하며, 점유를 성공할 때까지 Blocking 됩니다.

unique_lock은 객체를 미리 선언해두고, 나중에 필요한 시점에 락 점유를 시도할 수 있습니다. lock(), try_lock(), try_lock_for(), try_lock_until() 등의 함수도 제공합니다.


lock 예제

#include <mutex>

using namespace std;

class LockExample {
 public:
  void sendMessage(int what, int arg1, int arg2);

 private:
  mutable mutex mMutex;
};

void LockExample::sendMessage(int what, int arg1, int arg2) {
  unique_lock<mutex> lock<mMutex>

  mMessageSender->sendMessage(what, arg1, arg2);
}


이중 검사 락 알고리즘

멀티 쓰레드 환경에서 어떤 부분이 단 한 번만 호출이 되어야 한다면 이중 검사 락 알고리즘을 사용할 수 있습니다. 물론, call_once()를 사용한다면 굳이 이 알고리즘을 사용할 필요는 없을 것 같습니다.

아래 예제는 isInitialized 변수를 확인한 다음 초기화를 수행하고 isInitialized 변수의 값을 true로 세팅하는 예제입니다. isInitialized 변수가 true로 세팅되기 직전에 다른 쓰레드가 끼어드는 것을 방지하기 위해서 이중 검사 락 알고리즘을 사용했습니다.

#include <cstdio>
#include <thread>
#include <mutex>
#include <vector>

using namespace std;

class LockExample {
 public:
  void init() {
    printf("Init()\n");
  }
};

bool isInitialized = false;
mutex mtx;

LockExample exam;

void func() {
  if (isInitialized == false) {
    unique_lock<mutex> lock(mtx);

    if (isInitialized == false) {
      exam.init();
      isInitialized = true;
    }
  }

  printf("func()\n");
}

int main() {
  vector<thread> threads;

  for (int i = 0; i < 5; i++) {
    threads.push_back(thread{func});
  }

  for (auto &t : threads) {
    t.join();
  }

  return 0;
}