unique_lock는 쉽게 말하면 lock_guard에 기능이 더해진 것이라고 볼 수 있습니다.

lock_guard의 경우 정의와 동시에 락을 걸고, 파괴될 때만 락을 풀 수 있습니다.

 

그에 비해 unique_lock은 정의와 동시에 락을 걸 수도 있고, 또는 뒤에 락을 걸 수도 있습니다. 또 락을 푸는 것도 원하는 대로 조절이 가능하며 try_lock이나 try_lock_for, try_lock_until 등도 지원합니다. 그리고 unique_lock끼리 소유한 뮤텍스 객체를 서로 교환도 할 수 있으며, 뮤텍스 객체를 얻거나 해제할 수도 있습니다.

 

간단한 경우에는 lock_guard가 사용하기 편하지만 뮤텍스를 다양하게 사용하고 싶은 경우에는 unique_lock이 훨씬 더 좋습니다.

 

 

unique_lock 생성

unique_lock 클래스의 생성자는 다양한 방법으로 뮤텍스를 사용할 수 있도록 여러 버전이 준비 되어 있습니다.

 

unique_lock() noexcept;

unique_lock(unique_lock&& Other) noexcept;

explicit unique_lock(mutex_type& Mtx);

unique_lock(mutex_type& Mtx, adopt_lock_t Adopt);

unique_lock(mutex_type& Mtx, defer_lock_t Defer) noexcept;

unique_lock(mutex_type& Mtx, try_to_lock_t Try);

template<class Rep, class Period>

   unique_lock(mutex_type& Mtx,

      const chrono::duration<Rep, Period> Rel_time);

template<class Clock, class Duration>

   unique_lock(mutex_type& Mtx,

      const chrono::time_point<Clock, Duration> Abs_time);

unique_lock(mutex_type& Mtx,

   const xtime *Abs_time) noexcept;

 

unique_lock의 사용 예는 아래와 같습니다.

 

std::mutex m;

....

// 락을 만들고 동시에 뮤텍스의 소유권을 가진다

std::unique_lock lck(m, std::adopt_lock);

 

 

std::mutex m;

....

// 락을 만들지만 뮤텍스의 소유권은 가지지 않는다

std::unique_lock lck(m,std::defer_lock);      

...

if (lck.try_lock()) {

....

}                   

 

 

std::unique_lock<std::timed_mutex>

        lk(m,std::chrono::milliseconds(3));

....

if(lk) {

    process(data);

}                   

 

 

 

unique_lock 멤버 함수

 

- void lock();

- mutex_type *mutex() const _NOEXCEPT;

- explicit operator bool() _NOEXCEPT

- unique_lock& operator=(

    unique_lock&& Other

  ) _NOEXCEPT;

- bool owns_lock() const _NOEXCEPT;

- mutex_type *release() _NOEXCEPT;

- void swap(

    unique_lock& Other

  ) _NOEXCEPT;

- bool try_lock() _NOEXCEPT;

- template<class Rep,

    class Period>

  bool try_lock_for(

     const chrono::duration<Rep,

     Period>& Rel_time

  );

- template<class Clock, class Duration>

    bool try_lock_until(const chrono::time_point<Clock, Duration>& Abs_time);

    bool try_lock_until(const xtime *Abs_time);

- void unlock();

 

저작자 표시
신고
by 흥배 2013.07.15 08:00

std::mutex try_lock을 사용하면 뮤텍스의 소유권을 가질 수 있으면 true를 반환하고, 그렇지 못하면 바로 false를 반환하므로 소유권을 얻고 싶으면 또 다시 try_lock을 호출해야 합니다. 그런데 공유자원을 사용하는 시간이 아주 짧은 경우도 있다면 특정 시간까지만 계속 락을 시도하여 뮤텍스의 소유권을 가지기를 바라는 경우가 있습니다. 이럴 때 사용하는 것이 timed_mutex입니다.

 

timed_mutex std::mutex의 모든 기능을 다 가지고 있으면서 try_lock_for try_lock_until 함수 두 개가 더 있습니다. 이 함수들은 시간을 사용하여 뮤텍스 소유를 시도합니다. 시간은 std::chrono로 설정합니다.

 

try_lock_for는 지정한 시간 동안 락을 걸어봅니다.

template<class Rep, class Period>

   bool try_lock_for(const chrono::duration<Rep, Period>& Rel_time);

 

// 10초 동안 mutex의 소유권을 얻을 때까지 시도

if( mutex.try_lock_for(std::chrono::seconds(10)) )

{

           // 공유 자원 사용

           mutex.unlock();

}

 

 

try_lock_until은 지정한 시간까지 뮤텍스의 소유를 시도합니다.

 

template<class Clock, class Duration>

   bool try_lock_for(const chrono::time_point<Clock, Duration>& Abs_time);

bool try_lock_until(const xtime *Abs_time);

 

std::chrono::system_clock::time_point CurTime = std::chrono::system_clock::now();

std::chrono::seconds dura_sec( 5 );

 

// 현재 시간에서 5초 후까지만 mutex를 소유하려고 시도한다

if( mutex.try_lock_until(CurTime + dura_sec) )

{

           // 공유 자원 사용

           mutex.unlock();

}


 

 

recursive_timed_mutex

recursive_timed_mutex std::mutex에 시간 기능이 추가된 timed_mutex 처럼 recursive_mutex에 시간 기능이 추가된 것입니다.

recursive_mutex에 추가된 함수나 사용 방법은 timed_mutex와 비슷합니다.

 

 

저작자 표시
신고
by 흥배 2013.07.08 08:00

std::mutex lock 멤버 함수의 설명을 보면 lock을 호출한 함수에서 unlock을 호출하지 않은 상태에서 또 다시 lock을 호출하면 알 수 없는 동작을 한다고 되어 있습니다.

 

스레드에서 lock을 호출한 후 다시 lock을 호출하는 경우는 아래와 같은 경우입니다.

 

class buffer {

    list<int> queue;

    std::mutex mut;

public:

    bool empty() {

        std::lock_guard<std::mutex> lock(mut);

        return queue.empty();

    }

    ...

    int pop() throw(out_of_range) {

        std::lock_guard <std::mutex> lock(mut);

        while (empty())

        {   

……..

        }

        int tmp = queue.front();

        queue.pop_front();

        return tmp;

     }

};

코드 출처 : http://d.hatena.ne.jp/hidemon/20081218/1229555003

 


위의 buffer 클래스를 보면 pop() 함수를 호출하면 먼저 mutex로 락을 건 다음 empty() 함수를 호출하는데 empty도 같은 mutex를 사용하여 lock을 걸고 있습니다. 즉 이미 락을 건 mutex에 또 lock을 걸고 있습니다. 이런 경우 데드락 상황에 빠질 수가 있습니다. 이 문제를 풀기 위해서는 recursive_mutex를 사용합니다.

recursive_mutex는 같은 스레드에서 lock을 건 후 또 다시 lock을 걸어도 괜찮습니다. lock을 건 횟수만큼 꼭 unlock을 호출해야 합니다.

 

그럼 아래는 위의 buffer 클래스를 recursive_mutex를 사용하여 올바르게 수정한 것 입니다. 단순하게 std::mutex std::recursive_mutex로 바꾸면 됩니다.

(std::mutex std::recursive_mutex의 사용 방법은 같습니다)

 

class buffer {

    list<int> queue;

    std::recursive_mutex mut;

public:

    bool empty() {

        std::lock_guard<std::recursive_mutex> lock(mut);

        return queue.empty();

    }

    ...

    int pop() throw(out_of_range) {

        std::lock_guard <std::recursive_mutex> lock(mut);

        while (empty())

        {   

……..

        }

        int tmp = queue.front();

        queue.pop_front();

        return tmp;

     }

};

 

 

저작자 표시
신고
by 흥배 2013.07.01 08:00

병렬 프로그래밍을 해보면 싫든 좋든 여러 스레드에서 하나의 객체에 동시에 접근하는 경우가 발생합니다. 앞선 std::thread에 관한 글을 보면 소개한 예제 중 그런 경우를 보실 수 있을 겁니다.

 

< 예제 >

#include <thread>

#include <iostream>

 

int main()

{

           std::thread Threads1( [] ()

              {

                        for( int i = 0; i < 5; ++i )

                        {

                                          std::cout << "Thread Num : " << i << std::endl;

                        }

              } );

 

 

           std::thread Threads2;

           Threads2 = std::thread( [] ()

              {

                        for( int i = 10; i < 15; ++i )

                        {

                                          std::cout << "Thread Num : " << i << std::endl;

                        }

              } );

 

           std::thread Threads3 = std::thread( [] ( int nParam )

              {

                      for( int i = 20; i < 25; ++i )

                      {

                                std::cout << "Thread Parameter : " << nParam << std::endl;

                      }

              }, 4 );

 

           getchar();

           return 0;

}

 

위 예제에서 ‘std::cout’ 각 스레드에 동시에 접근하고 있습니다. 그래서 실행하면 아래와 같은 결
과가 나옵니다.


실행 화면을 보면 출력 결과 중 일부가 뒤죽박죽 되어 있는 것을 알 수 있습니다. 이유는 Thread1 스레드에서 출력스트림 결과를 다 출력하기 전에 Threads2가 출력스트림을 사용하여 발생한 문제입니다. 이런 것을 data-race(데이터 경합) 이라고 합니다.

 

그런데 위 예제를 여러 번 실행해 보면 아래처럼 올바르게 나올 때도 있습니다.



사실 이런 부분이 병렬 프로그래밍의 어려움 중의 하나입니다. 여러 스레드에서 하나의 공유 자원을 사용할 때 타이밍에 의해서 순서대로 사용할 수도 있고 그렇지 못할 수도 있기 때문에 버그 발생이 언제 어떻게 생길지 알기 힘듭니다.

 

병렬 프로그래밍을 할 때는 언제나 공유 자원을 최소한으로 하고, 공유 자원은 꼭 동기화 객체를 사용하여 여러 스레드가 동시에 사용하지 못하도록 해야 합니다.

 

위의 문제는 동기화 객체를 사용하면 해결 할 수 있습니다. 여기서는 앞선 글에서 사용한 적이 있는 뮤텍스(std::mutex)를 사용하겠습니다.

 

그럼 위 예제를 mutex를 사용하여 문제를 해결해 보겠습니다.

 

<예제 1 >

#include <thread>

#include <iostream>

#include <mutex>

 

int main()

{

           std::mutex mtx_lock;

 

           std::thread Threads1( [&] ()

              {

                        for( int i = 0; i < 5; ++i )

                        {

                                          mtx_lock.lock();

                                          std::cout << "Thread Num : " << i << std::endl;

                                          mtx_lock.unlock();

                        }

              } );

 

 

           std::thread Threads2;

           Threads2 = std::thread( [&] ()

              {

                        for( int i = 10; i < 15; ++i )

                        {

                                          mtx_lock.lock();

                                          std::cout << "Thread Num : " << i << std::endl;

                                          mtx_lock.unlock();

                        }

              } );

 

           std::thread Threads3 = std::thread( [&] ( int nParam )

              {

                      for( int i = 20; i < 25; ++i )

                      {

                                 mtx_lock.lock();

                                 std::cout << "Thread Parameter : " << nParam << std::endl;

                                 mtx_lock.unlock();

                      }

              }, 4 );

 

          

 

           getchar();

           return 0;

}

 

< 실행 결과>



std::mutex를 사용하기 위해서는 아래의 헤더 파일을 추가합니다.

#include <mutex>

 

스레드에서 공유 자원을 사용할 때는 아래 처럼 락을 걸어서 뮤텍스 mtx_lock의 소유권을 얻어서 다른 스레드가 접근하지 못하도록 합니다. 그리고 공유 자원을 다 사용하였다면 락을 해제하여 mtx_lock의 소유권을 버립니다.

mtx_lock.lock();

std::cout << "Thread Num : " << i << std::endl;

mtx_lock.unlock();

 

락을 건 후(lock) 락을 풀(unlock) 때까지는 다른 스레드는 대기를 하고, 락을 풀면 대기하고 있는 스레드 중 하나가 락을 건 후 공유 자원을 사용합니다.

그러므로 락을 건 후에는 꼭 락을 풀어야 하고(만약 풀지 않으면 데드락 상황에 빠지게 됩니다), 락을 사용하는 부위가 너무 빈번하면(스레드 대기가 늘어나므로) 병렬 프로그래밍의 장점이 많이 약해집니다.

 

 

try_lock

std::mutex lock unlock 이외에 try_lock 이라는 멤버 함수가 있습니다.

bool try_lock();

 

스레드에서 lock을 호출할 때 다른 스레드가 이미 lock을 호출하여 뮤텍스의 소유권을 가진 후 아직 unlock을 호출하지 않았다면 그 자리에서 대기를 합니다. 그러나 try_lock은 대기를 하지 않고 false를 반환합니다(뮤텍스의 소유권을 얻지 못한 경우). 반대로 true를 반환한 경우는 뮤텍스에 대한 소유권을 가지게 되므로 공유자원을 안전하게 사용할 수 있습니다.

 

try_lock을 사용하는 경우는 만약 공유 자원을 다른 스레드에서 사용 중이면 스레드가 대기하면서 그냥 시간을 소비하지 않고 또 다른 작업을 하도록 할 때 사용하면 좋습니다.

 


 

ps : 참고로 std::mutex Windows OS에서는 내부적으로는 크리티컬섹션을 사용합니다.

 

 

저작자 표시
신고
by 흥배 2013.06.03 08:00
| 1 |

티스토리 툴바