c++ std::mutex

2 분 소요

std::mutex C++11

C++11 에서 std::thread가 등장하면서 std::mutex 가 추가되었다.
std::mutex클래스는 여러 스레드가 동시에 액세스하는 공유 데이터를 보호하는 데 사용할 수 있는 동기화 기본 요소다.
mutex는 독점적이고 비재귀적인 소유권 의미 체계를 제공한다:
  • lock이나 try_lock을 성공적으로 호출한 스레드는 mutexunlock이 호출 될 때까지 소유 한다.
  • 하나의 스레드가 mutex를 소유하면 다른 스레드들은 lock을 호출하면 block되고, try_lock을 호출하면 false를 반환한다.
  • 스레드가 lock또는 try_lock을 호출하기 전에 mutex를 소유하고 있으면 안된다.

lock, unlock

아래 그림과 같이 사용할 수 있다. 실제 해당되는 변수와 직접적인 관계가 없다. mutex 객체를 생성하고 lock 으로 권한을 획득하고 공유되는 데이터에 접근한다. 데이터에 관련된 작업을 마친 후에는 unlock을 호출해야 다른 thread가 접근 할 수 있다.
std::mutex mtx;
void function(void)
{
    mtx.lock();
    // Do something...
    mtx.unlock();
}

try_lock

try_lock 메서드는 lock메서드와는 다르게 return 값으로 현재 자원이 사용중인 아닌지 확인 할 수 있다.

Example

std::mutex mtx;
void function(void)
{
    if(mtx.try_lock())
    {
        //Do something...
        mtx.unlock();
    }
}
NOTE_ std::mutex는 일반적으로 바로 접근하지 못한다. std::unique_lock, std::lock_guard 아니면
std::scoped_lock을 사용하여 자원에 lock을 걸어주는 것이 더 exception-save한 방식이다.

std::lock_guard

  • 기본으로 제공하는 lock 이나 try_lock을 사용했을 때 unlock하지 않는 실수를 할 수 가 있는데 이때 해결을 도와주는 친구가 std::lock_guard이다.
  • unlock을 하기 전에 예외가 발생한다면 unlock 되지 않는 경우도 안전하게 unlock시켜준다.
std::lock_guard는 mutex를 소유하고 있는 코드 블럭의 기간 동안 RAII-style 매커니즘을 제공하는 mutex wrapper다.
NOTE_ RAII (Resource Acquistion Is Initialization)-style은 객체가 실제 사용되는 영역을 벗어나면 자원을 해제해주는 기법을 고려하여 설계하는 방법이다.

Example

#include <thread>
#include <mutex>
#include <iostream>
    
int g_i = 0;
std::mutex g_i_mutex;  // protects g_i
    
void safe_increment()
{
    const std::lock_guard<std::mutex> lock(g_i_mutex);
    ++g_i;
    
    std::cout << "g_i: " << g_i << "; in thread #"
                << std::this_thread::get_id() << '\n';
    
    // g_i_mutex is automatically released when lock
    // goes out of scope
}
    
int main()
{
    std::cout << "g_i: " << g_i << "; in main()\n";
    
    std::thread t1(safe_increment);
    std::thread t2(safe_increment);
    
    t1.join();
    t2.join();
    
    std::cout << "g_i: " << g_i << "; in main()\n";
}

Result

g_i: 0; in main()
g_i: 1; in thread #140581100328704
g_i: 2; in thread #140581091936000
g_i: 2; in main()

std::unique_lock

  • std::unique_locklock_guard의 동작을 조금 더 확장한 것이다.
  • RAII특성을 잃지 않으면서도 생성 시 lock을 시키지 않고 특정 시점에 lock시킬 수 있다.
  • std::unique_lock은 주로 두개 이상의 mutex를 사용할 때 deadlock 에 빠지지 않고 mutex를 잘 unlock할 수 있도록 사용한다.
  • 생성자에 인자로 mutex객체만 넘겨준다면 생성 시에 lock이 걸리게된다. 만약 생성자에 mutex와 함께 std::defer_lock, std::try_to_lock, std::adopt_lock을 같이 넣어주게 되면 초기 상태를 다르게 세팅 할 수 있다.
    • std::defer_lock: lock이 걸리지 않으며 잠금 구조만 생성된다. std::lock()함수로 lock할 수 있다.
    • std::try_to_lock: lock이 걸리지 않으며 잠금 구조만 생성된다. 내부적으로 try_lock()을 호출해서 소유권을 가져 오며 실패 시 false를 반환한다. lock.owns_lock() 등의 코드로 자신이 lock을 할 수 있는지 확인이필요하다.
    • std::adopt_lock: lock이 걸리지 않으며 잠금 구조만 생성된다. 현재 호출된 블럭의 스레드가 mutex의 소유권을 가지고 있다고 가정한다. (사용하려는 mutex 객체가 lock 되어 있는 상태여야 함) 이미 lock이 된 후 unlock을 하지 않더라도 unique_lock으로 unlock 가능하다.

Example

#include <mutex>
#include <thread>
#include <chrono>
    
struct Box {
    explicit Box(int num) : num_things{num} {}
    
    int num_things;
    std::mutex m;
};
    
void transfer(Box &from, Box &to, int num)
{
    // don't actually take the locks yet
    std::unique_lock<std::mutex> lock1(from.m, std::defer_lock);
    std::unique_lock<std::mutex> lock2(to.m, std::defer_lock);
    
    // lock both unique_locks without deadlock
    std::lock(lock1, lock2);
    
    from.num_things -= num;
    to.num_things += num;
    
    // 'from.m' and 'to.m' mutexes unlocked in 'unique_lock' dtors
}
    
int main()
{
    Box acc1(100);
    Box acc2(50);
    
    std::thread t1(transfer, std::ref(acc1), std::ref(acc2), 10);
    std::thread t2(transfer, std::ref(acc2), std::ref(acc1), 5);
    
    t1.join();
    t2.join();
}
위 예제는 두개의 mutex 객체를 std::defer_lock 으로 lock으로 초기화 하지 않고 한번에 std::lock() 으로 동시에 lock을 걸어 deadlock 이 걸리지 않게 된다. 만약 std::defer_lock 을 같이 넘겨주지 않으면 unique_lock 생성 시 바로 lock 으로 걸리기 때문에 deadlock 이 발생한다.

참고

태그: ,

카테고리:

업데이트: