c++ std::mutex
std::mutex C++11
C++11 에서std::thread가 등장하면서 std::mutex 가 추가되었다.
mutex는 독점적이고 비재귀적인 소유권 의미 체계를 제공한다:
lock이나try_lock을 성공적으로 호출한 스레드는mutex를unlock이 호출 될 때까지 소유 한다.- 하나의 스레드가 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_lock은lock_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();
}std::defer_lock 으로 lock으로 초기화 하지 않고 한번에 std::lock() 으로 동시에 lock을 걸어 deadlock 이 걸리지 않게 된다.
만약 std::defer_lock 을 같이 넘겨주지 않으면 unique_lock 생성 시 바로 lock 으로 걸리기 때문에 deadlock 이 발생한다.