mutex는 c++ 11에서 부터 추가되었습니다.
<mutex> 헤더를 통해서 접근할 수 있습니다.
Window의 Mutex(커널 모드)와는 이름이 동일 하지만
내부구현은 WINDOW의 CriticalSection or srwlock(유저 모드)을 통해서 구현되어 있습니다.
Mutex류 객체는 일반적으로 동시적으로 여러개의 스레드로부터 공유되는 자원을 보호 하기 위해서 사용되어 집니다.
주의 할 점은 mutex는 직접적으로 lock(), unlock()함수를 호출해서 사용되기 보다늗
std::lock_guard나 std::unique_lock, std::scope_lock 과 같은 lock 관리 객체를 통해서 사용되어집니다.
(exception에 대해서 안전성을 보장등등, 에러 발생 예방)
_Mutex_base
mutex 객체들은 _Mutex_base를 상속받아서 구현되어 있습니다.
Mutex를 설명하기 전에 _Mutex_base에 대해서 먼저 정리하겠습니다.
멤버 변수
class _Mutex_base { // base class for all mutex types
public:
// 생략..
private:
// 생략..
aligned_storage_t<_Mtx_internal_imp_size, _Mtx_internal_imp_alignment> _Mtx_storage;
_Mtx_t _Mymtx() noexcept { // get pointer to _Mtx_internal_imp_t inside _Mtx_storage
return reinterpret_cast<_Mtx_t>(&_Mtx_storage);
}
};
std에서 사용하는 모든 Mutex의 base 클래스입니다. 거의 대부분의 구현은 여기에 정의 되어 있습니다.
매개 변수 | 설 명 |
_Mtx_storage |
정렬된 메모리 구조의 변수를 사용합니다. 실제로 데이터 사용시에는 _Mymtx() 함수를 통해서 변환되어 사용되어 집니다. |
_Mtx_t 구조
변 수 | 설 명 |
get_cs() |
stl_critical_section_interface 객체를 생성합니다. 윈도우 버전에 따라서 사용하는 Lock 관련 변수가 달라집니다. (아래 설명) |
thread_id | Mutex를 소유한 threadId 저장 |
type |
Mutex 설정 Flag 값 (_Mtx_plain, _Mtx_try, _Mtx_timed, _Mtx_recursive) _Mtx_try는 기본값으로 설정됩니다. |
count | Mutex를 획득한 Count (Recursice Mutex에 사용 필요) |
윈도우의 CRITICAL_SECTION 객체에는 thread_id, count를 저장하는 공간이 있지만
SRWLock을 공통 인터페이스로 사용하기 위해서 해당 데이터를 중복으로 선언되도록 설계된 것 같습니다.
(나중에 이전 버전의 Mutex 구조를 확인해봐야 겟습니다.)
Window 버전별 객체 생성 구조 및 해제
class _Mutex_base { // base class for all mutex types
public:
_Mutex_base(int _Flags = 0) noexcept {
_Mtx_init_in_situ(_Mymtx(), _Flags | _Mtx_try);
}
void _Mtx_destroy_in_situ(_Mtx_t mtx) { // destroy mutex in situ
_THREAD_ASSERT(mtx->count == 0, "mutex destroyed while busy");
mtx->_get_cs()->destroy();
}
private:
// 생략..
};
_Mutex_base의 생성자를 살펴 보면 _Mtx_storage변수를 인자로 _Mtx_init_in_situ 함수를 호출합니다.
void _Mtx_init_in_situ(_Mtx_t mtx, int type) { // initialize mutex in situ
Concurrency::details::create_stl_critical_section(mtx->_get_cs());
mtx->thread_id = -1;
mtx->type = type;
mtx->count = 0;
}
_Mtx_init_in_situ함수에서 _Mtx_t타입으로 변환된 _Mtx_storage인자를 초기화 합니다.
여기서 주의해서 봐야 할 점은 Concurrency::details::create_stl_critical_section 함수를 호출한다는 점입니다.
inline void create_stl_critical_section(stl_critical_section_interface* p) {
#ifdef _CRT_WINDOWS
new (p) stl_critical_section_win7;
#else
switch (__stl_sync_api_impl_mode) {
case __stl_sync_api_modes_enum::normal:
case __stl_sync_api_modes_enum::win7:
if (are_win7_sync_apis_available()) {
new (p) stl_critical_section_win7;
return;
}
// fall through
case __stl_sync_api_modes_enum::vista:
if (are_vista_sync_apis_available()) {
new (p) stl_critical_section_vista;
return;
}
// fall through
case __stl_sync_api_modes_enum::concrt:
default:
#ifdef _STL_CONCRT_SUPPORT
new (p) stl_critical_section_concrt;
return;
#else
std::terminate();
#endif // _STL_CONCRT_SUPPORT
}
#endif // _CRT_WINDOWS
}
create_stl_critical_section 함수에서는 윈도우 버전의 API에 따라서 stl_critical_section_interface 공용 인터페이스를
쓰는 다른버전의 객체를 생성합니다.
윈도우 버전 | 설 명 |
WIN7 이상 |
stl_critical_section_win7 객체를 생성합니다. 내부적으로 SRWLock을 통해서 구현되어 있습니다. |
VISTA 이하 |
stl_critical_section_vista 객체를 생성합니다. 내부적으로 CRITICAL_SECTION 객체를 사용되어서 구현되어 있습니다. |
SRWLock이나 CRITICAL_SECTION에 대한 내용은 다른 글에 등록된 내용을 참조하시기 바랍니다.
https://jungwoong.tistory.com/19?category=1067195
https://jungwoong.tistory.com/18?category=1067195
객체의 소멸자 호출시에는 각 할당된 Lock 객체를 반환하도록 구성되어 있습니다.
std::mutex
특징
뮤텍스는 여러 스레드에 의해서 동시에 접근될 수 있는 자원에 대해서 배타적 접근이 가능하도록 잠금을 설정하여
공유자원에 대해서 안정성을 보장합니다.
사용시 주의 할 점은 이미 Mutex를 소유한 thread에서 중복으로 Lock을 호출 할 경우에는
Exception을 발생 시킵니다.
소스 코드
class mutex : public _Mutex_base { // class for mutual exclusion
public:
/* constexpr */ mutex() noexcept // TRANSITION, ABI
: _Mutex_base() {}
// 복사생성 and 대입연산 불가
// 복사를 지원하지 않습니다.
mutex(const mutex&) = delete;
mutex& operator=(const mutex&) = delete;
};
mutex의 구현 클래스를 확인해보면 위의 소스와 같이 간단하게 구현되어 있습니다.
복사생성자와 대입연산자가 delete 되어 있기 때문에 복사를 지원하지 않습니다.
실질적인 구현은 _Mutex_base 클래스에 구현되어 있음을 알 수 있고 다른 Mutex류들도 _Mutex_base를 상속받도록
구현 되어 있을것으로 예상됩니다.
함수
함수명 | 설 명 |
lock | 뮤텍스에 대한 잠금을 시도하고 이미 잠긴 경우 block상태로 대기합니다. |
try_lock | 뮤텍스에 대한 잠금을 시도하고 잠금을 획득하면 true, 이미 잠긴 경우 false를 리턴합니다. |
unlock | 뮤텍스 잠금을 해제합니다. |
예 제
std::mutex g_num_mutex;
static unsigned int g_num = 0;
constexpr int rountine_count = 100;
void lock_increment(int id)
{
for (int i = 0; i < rountine_count; ++i) {
g_num_mutex.lock();
++g_num;
std::cout << id << " => " << g_num << '\n';
g_num_mutex.unlock();
}
}
void trylock_increment(int id)
{
int count = 0;
do
{
// 뮤텍스 획득 성공
if (g_num_mutex.try_lock())
{
++g_num;
count++;
std::cout << id << " => " << g_num << '\n';
g_num_mutex.unlock();
}
// else 뮤텍스 획득 실패
} while (count < rountine_count);
}
int _tmain(int argc, _TCHAR* argv[])
{
std::thread t1(lock_increment, 0);
std::thread t2(trylock_increment, 1);
t1.join();
t2.join();
}
예제를 보면 lock과 trylock을 통해서 뮤텍스 객체를 잠금을 획득하고 unlock을 통해서 뮤텍스 객체를 잠금을 해제
해서 공유 자원인 g_num를 안전하게 사용하는 것을 확인 할 수 있습니다.
int _tmain(int argc, _TCHAR* argv[])
{
std::mutex m;
try
{
m.lock();
// exception 발생
m.lock();
m.unlock();
}
catch(std::exception e)
{
std::cout << e.what() << std::endl;
}
}
mutex를 사용할 때 주의 할 점은 만약에 같은 스레드에서 여러번의 락 함수를 호출하면 exception을 발생시킵니다.
만약 이런 상황이라면 std::recursive_mutex를 사용하시면 됩니다.
수행 속도
VS2019기준 mutex의 수행 속도의 경우 내부적으로 SRWLOCK을 wrapper해서 사용되어 지기 때문에 추가적인
작업으로 인해서 SRWLOCK보다 느리게 동작합니다.
https://jungwoong.tistory.com/5
std::recursive_mutex
mutex 객체의 중복 호출 결과 입니다.
Mutex와 동일하게 _Mutex_base를 상속받아 구현되어 있습니다. 기능은 거의 비슷하지만
같은 스레드가 recursive_mutex를 여러번 획득하는 것을 허용합니다.(mutex는 exception 발생)
내부적으로 count값을 가지고 있어서 획득 횟수를 소유하며 해제를 위해서는 획득한 횟수만큼 unlock함수를
호출 해야합니다.
std::timed_mutex
timed_mutex는 내부적으로 mutex와 condition_variable(조건 변수)로 구성되어 있습니다.
condition_variable를 통해서 mutex를 대기하는데 덕분에 특정시간을 지정해서 lock을 설정할 수 있습니다.
함수명 | 설 명 |
try_lock_until | 뮤텍스를 획득하려고 시도합니다. 입력한 chrono::time_point 까지 획득하지 못하면 false로 리턴됩니다. |
try_lock_for | 뮤텍스를 획득하려고 시도합니다. 입력된 chrono::duration을 대기 하고 그때 까지 획득하지 못하면 false로 리턴됩니다. |
위의 함수들은 timed_mutex가 가지고 있는 추가적인 함수입니다. 기본적인 mutex 기능은 std::mutex 객체와
동일 합니다.
std::shared_mutex
cppreference.com문서에서는 c++ 17에서 지원한다고 적혀 있는데 visual studio에서는 c++ 14로 설정해도
사용 가능 합니다.
shared_timed_mutex는 c++14에서 지원하니 대체해서 사용 가능합니다.
기 능
shared_mutex는 스레드간 공유 데이터에 접근을 제어하는데 사용됩니다.
그러나 다른 mutex 객체와 달리 2가지 타입의 접근 방식을 제공합니다.
- shared : 여러 스레드들이 동일한 mutex 소유권을 공유 할 수 있습니다.
- exclusive : 오직 하나의 스레드만 소유권을 소유합니다.
만약에 스레드가 exclusive lock을(lock이나 try_lock을 통해서) 획득하면 다른 스레드들은 lock을 획득할 수
없습니다. ( shared 타입도 포함합니다.)
만약에 스레드가 shared lockd을(lock_shared, try_lock_shared를 통해서) 획득하면 다른 스레드들은 exclusive lock을
획득할 수 없지만 shared lock은 획득 할 수 있습니다.
exclusive lock이 획득 되지 않은 경우에만 shared lock이 여러 스레드에 의해서 획득 될 수 있습니다.
하나의 스레드는 오직 하나의 타입의 lock만 획득 할 수 있습니다.( recusive 안됨!!)
shared_mutex에는 여러 스레드에서 동시에 읽기 작업이 많은 경우 효율적입니다.
함 수
함수명 | 설 명 |
lock | 뮤텍스에 대해서 exclusive lock 획득을 시도합니다. |
try_lock | try형태로 뮤텍스에 대해서 exclusive lock 획득을 시도합니다. |
lock_shared | 뮤텍스에 대해서 shared lock 획득을 시도합니다. |
try_lock_shared | try형태로 뮤텍스에 대해서 shared lock 획득을 시도합니다. |
unlock | exclusive 타입으로 lock의 잠금을 해제합니다. |
unlock_shared | shared 타입으로 lock의 잠금을 해제합니다. |
예 제
class ExampleObject {
public:
// count 객체를 읽는 작업을 위해서 shared 모드로 mutex를 획득합니다.
UINT64 get() {
std::shared_lock<std::shared_mutex> l(sm);
return count;
}
// count 객체를 쓰는 작업을 위해서 exclusive 모드로 mutex를 획득합니다.
void Add(UINT64 val) {
std::unique_lock<std::shared_mutex> l(sm);
count += val;
}
private :
std::shared_mutex sm;
UINT64 count;
};
int main()
{
auto peo = std::make_shared<ExampleObject>();
std::thread read_thread([peo] {
// shared 모드로 데이터를 읽습니다
for (int i = 0; i < 1000; i++) {
std::cout << peo->get() << std::endl;
}
});
std::thread write_thread([peo] {
// exclusive 모드로 데이터를 씁니다.
for (int i = 0; i < 1000000; i++) {
peo->Add(i);
}
});
read_thread.join();
write_thread.join();
}
std::shared_lock을 사용하면 shared모드로 락을 획득하고 객체 소멸자에서 자동으로 락을 해제 합니다.
std::unique_lock을 사용하면 exclusive 모드로 락을 획득하고 객체 소멸자에서 자동으로 락을 해제 합니다.
내 부 구 조
std::shared_mutex의 내부 구조는 단순하게 구현되어 있습니다.
class shared_mutex { // class for mutual exclusion shared across threads
public:
using native_handle_type = _Smtx_t*;
// 생략
...
private:
// _Smtx_t는 void* 입니다.
_Smtx_t _Myhandle;
};
_Myhandle이라는 void형 포인터를 하나만 들고 있을 뿐이라서 매우 빠르게 동작이 가능합니다.
shared mode로 lock획득시에는 _Myhandle의 값이 0x0x0000000000000011로 셋팅됩니다.
exclusive mode로 lock획득시에는 _Myhandle의 값이 0x0x0000000000000001로 셋팅됩니다.
이전 글에서 std::shared_mutex와 비슷하게 동작하는 window api 함수인 SWRLock을 설명 드렸는데
내부 구조가 동일합니다. 아마도 Window에서는 해당 기능을 기반으로 구현된 것으로 생각됩니다.
https://jungwoong.tistory.com/19?category=1067195
참조
https://en.cppreference.com/w/cpp/thread/mutex
'c++ > modern c++' 카테고리의 다른 글
[c++] condition variable(조건 변수) (0) | 2020.07.10 |
---|---|
[c++] std::lock 관련 함수들 (0) | 2020.05.22 |
[c++17] string_view (0) | 2020.03.21 |
[c++] constexpr 키워드 (0) | 2020.03.19 |
[c++] auto와 decltype 키워드 (0) | 2020.03.15 |