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

 

[Window c++] 슬림 리더-라이터 락 (SWRLock)

SRWLock(Slim Reader-Writer Lock)은 크리티컬 섹션과 마찬가지로 공유자원을 다수의 스레드가 접근할 때 공유자원을 보호할 목적으로 사용합니다. 크리티컬 섹션과의 차이점은 공유 자원을 읽기만 하는 스레드와..

jungwoong.tistory.com

https://jungwoong.tistory.com/18?category=1067195

 

[Window c++] 크리티컬 섹션

크리티컬 섹션은 스레드 간 공유 자원에 대해서 배타적으로 접근해야 하는 작은 코드의 집합을 의미합니다. 공유자원를 "원자적"으로 수행 할 수 있도록 하는데 "원자적"이란 공유자원에 현재 스레드가 접근 중에..

jungwoong.tistory.com

객체의 소멸자 호출시에는 각 할당된 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

 

[windows c++] 유저모드 스레드 동기화 방법별로 속도 측정

윈도우에서 스레드 간의 동기화를 위한 다양한 방법을 사용해서 속도를 측정해보도록 합니다. 테스트 환경 : visual studio 2017, cpu i7-4790(코어4개, 로직스레드 8개) ElapsedTimeOutput클래스 객체는 https://..

jungwoong.tistory.com

 

std::recursive_mutex

mutex의  같은 스레드에서 중복 호출 동작 

 mutex 객체의 중복 호출 결과 입니다.

recursive_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 획득시

 shared mode로 lock획득시에는 _Myhandle의 값이 0x0x0000000000000011로 셋팅됩니다.

exclusive mode lock 획득시

 exclusive mode로 lock획득시에는 _Myhandle의 값이 0x0x0000000000000001로 셋팅됩니다.

 이전 글에서 std::shared_mutex와 비슷하게 동작하는 window api 함수인 SWRLock을 설명 드렸는데 

 내부 구조가 동일합니다. 아마도 Window에서는 해당 기능을 기반으로 구현된 것으로 생각됩니다. 

https://jungwoong.tistory.com/19?category=1067195

 

[Window c++] 슬림 리더-라이터 락 (SWRLock)

SRWLock(Slim Reader-Writer Lock)은 크리티컬 섹션과 마찬가지로 공유자원을 다수의 스레드가 접근할 때 공유자원을 보호할 목적으로 사용합니다. 크리티컬 섹션과의 차이점은 공유 자원을 읽기만 하는

jungwoong.tistory.com

 

참조 

https://en.cppreference.com/w/cpp/thread/mutex

 

std::mutex - cppreference.com

class mutex; (since C++11) The mutex class is a synchronization primitive that can be used to protect shared data from being simultaneously accessed by multiple threads. mutex offers exclusive, non-recursive ownership semantics: A calling thread owns a mut

en.cppreference.com

 

'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

+ Recent posts