C++ 뮤텍스(std::mutex)

C++ Mutex(std::mutex)

Posted by dydtjr1128 on April 05, 2020 · 5 mins read CPP

Intro

다양한 쓰레드에서 공유자원을 접근해 값을 변경, 읽는 작업을 수행한다면 실제로는 같은 순간에 수행되는것이 아니기 때문에 자신이 원했던 값이 아닌 올바르지 않은 값을 읽어 올 수도 있게 됩니다.

이러한 경쟁 상태(race condition)를 발생시키지 않도록 하기 위해서 C++에서는 mutex라는 객체를 지원합니다.

std::mutex

mutex는 여러 스레드의 공유자원에 대한 동시 접근을 막아 주는 역할을 합니다. C++에서는 많은 종류의 mutex를 지원합니다.

  • std::mutex(C++11)
  • std::recursive_mutex(C++11)
  • std::timed_mutex(C++11)
  • std::recursive_timed_mutex(C++11)
  • std::shared_timed_mutex(C++14)
  • std::shared_mutex(C++17)

모두 공유자원의 동시접근을 막아주는 역할을 하지만 차이점이 존재합니다. 모두 "<mutex>"헤더에 포합되어 있으며 주로 위에서 두가지의 뮤텍스를 사용합니다.
std::mutex는 같은 mutex 객체로 여러 번 잠그면 데드락(deadlock) 현상이 발생합니다.
그래서 std::system_error를 던져주게 됩니다. 하지만 std::recursive_mutex는 같은 객체로 여러번 잠글수 있으며, 이 때에는 잠근 횟수 만큼 다시 unlock을 해 주어야 합니다.

std::timed_mutex는 try_lock_for() 과 try_lock_until()등의 메소드를 이용해서 일정시간, 지정한 시각까지 대기를 할 수 있는 기능을 가지고 있습니다.

C++14에서 추가된 std::shared_timed_mutex는 베타적 잠금 외에 한가지 접근가능한 기능을 통해 2가지의 접근 조건을 가질 수 있습니다. 하나의 쓰레드만 접근 가능하도록 막는 exclusive 상태와 몇개의 쓰레드가 동시에 접근 가능한 shared 상태가 존재합니다.

C++17에서 추가된 std::shared_mutex는 C++14의 std::shared_timed_mutex과 동일한 역할을 하지만 시간과 관련된 메소드가 빠진 클래스라고 볼 수 있습니다. 하지만 내부적으로 condition_variable로 구현된 std::shared_timed_mutex에 비해 Slim Reader Writer Lock (SRW Lock)로 구현된 std::shared_mutex가 훨씬 빠르고 높은 성능을 보여줍니다. => SRW Lock 에 대한 글

Appearance

Constructor
constexpr mutex() noexcept;     (since C++11)
mutex( const mutex& ) = delete; (since C++11)
Destructor
~mutex()

Example

#include <iostream>
#include <map>
#include <string>
#include <chrono>
#include <thread>
#include <mutex>

std::map<std::string, std::string> g_pages;
std::mutex g_pages_mutex;

void save_page(const std::string& url)
{
   std::this_thread::sleep_for(std::chrono::seconds(2));
   std::string result = "fake content";

   //std::lock_guard<std::mutex> guard(g_pages_mutex);
   g_pages_mutex.lock();
   g_pages[url] = result;
   g_pages_mutex.unlock();
}

int main()
{
   std::thread t1(save_page, "http://foo");
   std::thread t2(save_page, "http://bar");
   t1.join();
   t2.join();

   for (const auto& pair : g_pages) {
      std::cout << pair.first << " => " << pair.second << '\n';
   }
}

위의 코드에서 유심히 살펴보아야 할 부분은 save_page 메소드의 g_pages_mutex.lock();부분 입니다.
위의 코드는 2개의 쓰레드를 생성해 내용을 크롤링 해온다는 가정하에 짜여진 코드입니다. 각 url에 해당하는 내용을 채워 넣기 위해 map을 사용합니다.

하지만 map은 동기화가 보장되지 않는 non-thread-safe 클래스 이므로 이 map이라는 공유자원에 대해서 2개의 쓰레드가 동시 접근 할 경우 문제를 발생 시킬 수 있습니다.
따라서 이 공유자원에 대해 접근 할 때 하나의 쓰레드만 데이터를 넣어 줄 수 있도록 lock을 걸어 주어야 합니다.
데이터를 다 map에 넣은 후 다른 쓰레드가 접근 할 수 있도록 unlock으로 풀어주는 것입니다.

이러한 mutex의 불편한 점으로는 lock을 해준다면 필수적으로 unclock을 해 주어야 합니다.
unlock을 해 주지 않는다면 다른 쓰레드는 평생 lock에 걸린채로 나올 수 없게되는 deadlock 상태가 되게 됩니다.

이러한 불편함과 실수로 unlock을 하지 않을 수도 있는 상태를 방지하기위해 std::lock_guard라는 클래스가 존재합니다.
이 클래스는 객체가 생성 될때 생성자에서 lock를 하며, 이 객체가 소멸 될 때 unlock 됩니다. 이러한 장점으로 생성만 하면 알아서 lock이 되며 unlock을 걱정하지 않을 수 있습니다.

위의 save_page 메소드를 다음과 같이 짧고 깔끔하게 바꿀 수 있습니다.

void save_page(const std::string& url)
{
   std::this_thread::sleep_for(std::chrono::seconds(2));
   std::string result = "fake content";

   std::lock_guard<std::mutex> guard(g_pages_mutex);//lcok
   g_pages[url] = result;
}//guard의 소멸자가 호출 될 때(스택이 끝날때) unlock

Reference

https://en.cppreference.com/w/cpp/thread/mutex