다양한 쓰레드에서 공유자원을 접근해 값을 변경, 읽는 작업을 수행한다면 실제로는 같은 순간에 수행되는것이 아니기 때문에 자신이 원했던 값이 아닌 올바르지 않은 값을 읽어 올 수도 있게 됩니다.
이러한 경쟁 상태(race condition)
를 발생시키지 않도록 하기 위해서 C++에서는 mutex라는 객체를 지원합니다.
mutex는 여러 스레드의 공유자원에 대한 동시 접근을 막아 주는 역할을 합니다. C++에서는 많은 종류의 mutex를 지원합니다.
모두 공유자원의 동시접근을 막아주는 역할을 하지만 차이점이 존재합니다. 모두 "<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 에 대한 글
constexpr mutex() noexcept; (since C++11)
mutex( const mutex& ) = delete; (since C++11)
~mutex()
#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
https://en.cppreference.com/w/cpp/thread/mutex