본문 바로가기
💻 programming/c++

[c++] mutex, lock guard, unique lock, shared mutex, recursive mutex

by 연구원-A 2020. 12. 16.
반응형

medium.com/swlh/c-mutex-write-your-first-concurrent-code-69ac8b332288

 

[C++] MUTEX: Write Your First Concurrent Code

Learn to design concurrent code and to protect shared with a mutex by implementing your first thread-safe queue!

medium.com


Mutex가 뭘까?

Mutex는 여러 스레드에서 동시에 접근할 수 있는 공유 자원의 소유권을 결정하는 모델이다

Mutex를 이용해 다른 스레드에서 이 공유 자원에 접근하지 못하게 막을 수 있다는 의미다

 

만약 스레드 A에서 lock을 호출하면 unlock이 수행될 때까지 다른 스레드 B, C, D는 공유 자원에 접근할 수 없다

 

스레드 A가 소유권을 가지고 있는데도 스레드 B가 lock을 호출하면 unlock될 때까지 (소유권이 해제될 때까지) 대기하게 된다

try_lock을 호출했을 때에는 소유권 해제를 기다리지 않고 바로 False를 반환한다

 

무엇보다 중요한 것은 한 번 lock 호출을 한 스레드는 절대 lock 또는 try_lock을 두 번 호출해서는 안된다는 것이다

 

이미 lock을 호출해서 소유권을 가진 스레드가 lock을 다시 호출해버리면 이제 unlock 해줄 스레드가 없다

해당 Mutex를 이용해 소유권을 요청하는 모든 스레드가 대기 상태에 빠진다


Mutex 사용하기

  • 헤더파일: #include <mutex>
  • 선언: std::mutex mutex_name;
  • mutex 얻는 방법: mutex_name.lock( )
  • mutex 해제 방법: mutex_name.unlock( )
#include <mutex>
#include <vector>
std::mutex door;    // mutex 선언
std::vector<int> v; // 공유 자원
door.lock();
/*-----------------------*/
/* 이 공간은 thread-safe 영역에 해당됩니다. 오직 하나의 스레드만 접근할 수 있습니다.
 * vector v에 대한 유일한 소유권을 보장할 수 있습니다.
 */
/*-----------------------*/
door.unlock();

그럼 Lock guard는 뭘까?

앞서 말한 것처럼 개발자가 직접 mutex를 lock하고 unlock하면 휴먼 에러가 발생할 수 있다 (내가 아까 unlock을 호출했던가?)

 

짜고 있던 함수가 길-어져서 unlock을 깜빡한다면

스레드 A가 종료된 후에도 소유권이 해제되지 않으므로 다른 스레드들이 공유 자원에 접근할 수 없게 되는 불상사가 발생할 수 있다

 

같은 의미로 unlock이 호출되기 전에 함수 내부에서 예외가 발생하여 스레드가 종료돼도 문제가 발생한다

항상 예외 처리를 통해 unlock을 호출해야 하는 번거로움이 생긴 것이다

 

이러한 문제를 해결하기 위해 (= mutex의 unlock을 보장하기 위해⭐)

c++에서는 std::lock_guard, std::unique_lock와 같은 메서드를 제공하고 있다

 

lock_guard의 인자로 mutex가 전달되면

  • lock_guard의 생성자에서 lock이 호출되고
  • lock_guard의 소멸자에서 unlock이 호출되어 자동으로 소유권을 해제한다

 

참고: lock_guard는 RAII (Resource Acquisition Is Initialization) 철학에 따라 설계되었다

객체가 실제 사용되는 영역을 벗어나면 자원을 해제해야 한다는 의미로 객체가 소멸되면 소유권도 해제되어야 한다

 

std::lock_guard<std::mutex> lock_guard_name(raw_mutex);
#include <mutex>
#include <vector>
std::mutex door;    // mutex declaration
std::vector<int> v;
{     
     std::lock_guard<std::mutex> lg(door); 
     /* lg Constructor called. Equivalent to door.lock();
      * lg allocated on the stack */
     /*-----------------------*/
       
        /* Unique ownership of vector guaranteed */
    
     /*-----------------------*/
} /* lg exits its scope. Destructor called. 
  Equivalent to door.unlock(); */

Unique lock, mutex 자유롭게 사용하기

std::unique_lock은 lock_guard의 동작을 조금 더 확장한 것으로 lock_guard는 객체가 생성될 때 lock을 호출했다면,

옵션을 이용해서 lock을 호출하는 시점을 구분할 수 있다

 

unique_lock에 전달할 수 있는 옵션은 3가지가 있다 (std::defer_lock, std::try_to_lock, std::adopt_lock)

  • std::defer_lock: 두 개의 mutex를 교착 상태 없이 lock 하기 위해 생성 자에서 lock하지 않고 잠금 구조만 생성
  • std::adopt_lock: 이미 lock 되어있는 mutex의 lock을 잘 해제하기 위해 소유권을 가져옴 (실질적인 lock 없음)
  • std::try_to_lock: 생성자에서 lock이 대신 try_to_lock을 수행
// unique_lock constructor example
#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex, std::lock, std::unique_lock
                          // std::adopt_lock, std::defer_lock
std::mutex foo,bar;

void task_a () {
  std::lock (foo,bar);         // simultaneous lock (prevents deadlock)
  std::unique_lock<std::mutex> lck1 (foo,std::adopt_lock);
  std::unique_lock<std::mutex> lck2 (bar,std::adopt_lock);
  std::cout << "task a\n";
  // (unlocked automatically on destruction of lck1 and lck2)
}

void task_b () {
  // foo.lock(); bar.lock(); // replaced by:
  std::unique_lock<std::mutex> lck1, lck2;
  lck1 = std::unique_lock<std::mutex>(bar,std::defer_lock);
  lck2 = std::unique_lock<std::mutex>(foo,std::defer_lock);
  std::lock (lck1,lck2);       // simultaneous lock (prevents deadlock)
  std::cout << "task b\n";
  // (unlocked automatically on destruction of lck1 and lck2)
}


int main ()
{
  std::thread th1 (task_a);
  std::thread th2 (task_b);

  th1.join();
  th2.join();

  return 0;
}

언제 unique_lock을 사용하면 될까?

  • 항상 리소스가 lock되어 있을 필요가 없을 때
  • condition_variable을 사용할 때
  • shared_mutex를 exclusive mode로 잠글 때 (아래 서술)

Shared mutex + Shared lock?

mutex의 lock을 호출하면 모든 스레드가 대기 상태에 빠지고 공유자원에 접근할 수 없게 된다

 

안정적으로 병렬 처리를 수행할 수 있겠지만 이게 최선일까?

예를 들어, 여러 스레드에서 공유 자원을 읽기만 하는 건 문제가 없다 (데이터를 변경하지 않기 때문에)

 

공유 자원을 읽기만 하려는 스레드의 접근도 막는다면 처리 속도가 그만큼 늦어지므로 효율적이지 않다

 

그래서 c++17부터 std::shared_mutex 모델을 도입했다

  • Shared access
    • shared_lock을 호출해 소유권을 요청한다
    • shared_lock을 호출하면 shared_lock을 호출한 다른 스레드는 해당 자원을 공유할 수 있다 (다 같이 읽는 건 OK)
    • unique_lock을 호출한 스레드는 shared_lock이 종료될 때까지 기다린다 (읽는 건 OK지만, 쓰는 건 대기)
  • Exclusive access
    • unique_lock을 이용해 소유권을 요청한다
    • unique_lock을 호출하면 다른 모든 스레드는 해당 자원에 접근할 수 없다

SYNTAX

  • Header | #include <shared_mutex>;
  • Declaration | std::shared_mutex raw_sharedMutex;
  • To lock it in shared mode |
    • std::shared_lock<std::shared_mutex> sharedLock_name(raw_sharedMutex);
  • To lock it in exclusive mode |
    • std::unique_lock<std::shared_mutex> uniqueLock_name(raw_sharedMutex);

Code

#include <shared_mutex>
#include <vector>
std::shared_mutex door; //mutex declaration
std::vector<int> v;
int readVectorSize() {
    /* multiple threads can call this function simultaneously
     * no writing access allowed when sl is acquired */    
    
    std::shared_lock<std::shared_mutex> sl(door);
    return v.size();
}
void pushElement(int new_element) {
    /* exclusive access to vector guaranteed */
    
    std::unique_lock<std::shared_mutex> ul(door);
    v.push_back(new_element);
}

 

Recursive mutex?

같은 스레드에서 mutex lock이 여러 번 호출되면 안된다고 하던데

혹시 재귀적으로 호출되는 함수에서도 병렬처리를 할 순 없을까?

 

그 방법은 아래 std::recursive_mutex에서..

2020/12/16 - [c++ language/library] - [c++] std::recursive_mutex

 

Mutex와 Lock guard의 확장?

2020/12/16 - [c++ language/library] - [c++] std::mutex

2020/12/16 - [c++ language/library] - [c++] std::timed_mutex

2020/12/16 - [c++ language/library] - [c++] std::shared_mutex

 

만일 mutex를 써야할 일이 있다면 unlock()이 보장되는 lock_guard, unique_lock, shared_lock, scoped_lock을 사용하자

그리고 데이터 병렬처리를 해야할 일이 있다면 thread 보다는 future에서 제공하는 task 방식을 사용하는 것이 좋다

2021.08.04 - [programming/c++] - [c++] thread vs task (thread 와 async)

반응형

'💻 programming > c++' 카테고리의 다른 글

[c++] std::recursive_mutex (std::mutex 비교)  (0) 2020.12.16
[c++] std::mutex  (0) 2020.12.16
[c++] template meta programming  (0) 2020.12.15
[c++] variadic template (가변 길이 템플릿)  (0) 2020.12.15
[c++] functor (function object)  (0) 2020.12.15

댓글