이번 글에서는 std::condition_variable에 대해서 설명드립니다.

설명 및 기본 동작

std::condition_variable

 condition_variable 클래스는 다른 스레드가 공유 변수를 수정하고 condition_variable로 통지할 때까지

 스레드나 여러 스레드를 대기하도록 하는 데 사용 할 수 있는 동기화 기본 기법입니다. 

 언제나 뮤텍스와 연동되어서 동작해서 스레드에 안전하게 동작합니다. 

통지 스레드 동작 방식

 변수를 수정하려는 스레드는 다음과 같은 동작을 수행합니다. 

통지 스레드의 수행 동작

  1. std::mutex를 획득합니다. (일반적으로 std::unique_lock이나 std::lock_guard를 통해서)
  2. lock을 잡고 있는 동안 공유 변수를 수정작업을 수행합니다. 
  3. std::condition_variable의 notify_one또는 notify_all을 실행 합니다. 

 공유 변수가 atomic 일때도 대기 중인 스레드에 수정을 정확히 알리기 위해서는 뮤텍스 안에서 수정해야 합니다.

대기 스레드 동작 방식

 condition_variable을 통해서 대기하려는 스레드들은 다음과 같은 동작을 수행합니다. 

  1. 공유 변수을 보호하는데 사용하는 뮤텍스를 std::unique_lock<std::mutex>로 획득합니다. 
  2. 이미 업데이트와 통지가 진행된 경우 상태를 체크해서 확인 합니다. 
  3. wait 또는 wait_for, wait_until을 수행합니다. 대기 작업은 원자적으로 mutex를 반납하고 스레드의 수행을 일시중지합니다. 
  4. condition variable이 통지 되거나 시간이 만료되거나 거짓 통지가 발생하면 스레드는 깨어나고 원자적으로 뮤텍스를 획득합니다. 스레드는 condition을 체크해서 거짓 통지라면 다시 대기로 전환합니다. 

 std::condition_variable은 오직 std::unique_lock<std::mutex>만 함께 사용할 수 있습니다. 

 이러한 제한 사항 덕분에 특정 플랫폼에서는 성능 향상의 효과가 있습니다. 

 만약에 std::unique_lock이 아닌 BasicLockable한 객체로 condition variable기능을 사용하려면

 std::condition_variable_any를 사용 할 수 있습니다. (예를 들어 std::shared_lock이 있습니다.)

 condition variable의 wait, wait_for, wait_until, notify_one, notify_all 멤버 변수는 동시성 환경에서 호출을 지원합니다. 

 

멤버 함수

notify_one 함수

void notify_one() noexcept;

 condition variable에 대해서 대기하고 있는 하나의 스레드를 깨웁니다.

 스레드에 대한 통지 동작은 스레드에 안전하게 수행됩니다.

 그렇기 때문에 통지를 지연시켜서 notify_one()을 호출한 이후에 wait()함수를 호출하는 대기 스레드에게 

 통지를 전달할 수 없습니다. 

 윈도우10 에서 해당 함수의 구현을 따라가보면은 kernel32의 "WakeConditionVariable"로 구현되어 있습니다. 

 MSDN의 해당 함수의 설명을 번역해보면 WakeConditionVariable은 단일 스레드만 깨우며

 윈도우의 자동 재설정 이벤트 커널 객체를 사용하는 것과 유사하다고 적혀 있습니다. 

 윈도우의 이벤트 객체 사용법에 대해서 알고 있다면 더욱 쉽게 이해 할 수 있습니다. 

<예제>

std::mutex				m_;
std::condition_variable		cv_;
std::queue<std::string>		con_;

// queue에 데이터를 추가하고 대기하고 있는 스레드들에게 알림을 통지합니다. 
void Push(const std::string& obj) {
    {
        std::unique_lock<std::mutex> sl(m_);
        con_.push(obj);
    }

    // 대기중인 하나의 스레드 알림 통지
    cv_.notify_one();
    // 대기중인 모든 스레드 알림 통지
    // cv_.notify_all();
}

 

notify_all 함수

void notify_all() noexcept;

 notify_one()과 동일하게 동작하며 차이점은 condition variable에 대해서 대기하고 있는 모든 스레드를 깨웁니다.

 윈도우에서 해당 함수의 구현을 따라가보면은 kernel32의 "WakeAllConditionVariable"로 구현되어 있습니다. 

 윈도우의 수동 리셋 이벤트 커널 객체에 pulsing 되는 것과 비슷하게 동작합니다.

 

wait 함수

void wait(unique_lock<mutex>& _Lck);

template <class _Predicate>
void wait(unique_lock<mutex>& _Lck, _Predicate _Pred);

 이 함수는 condition_variable에서 통지를 받을 때까지 현재의 스레드를 중단 합니다. 

 선택적으로 전달된 조건이 만족할 때만 동작하도록 설정 할 수 있습니다. 

 wait()가 수행되면 내부적으로 스레드에 안전하게 락을 해제하고 현재 수행 중인 스레드를 중단하고 

 대기 스레드 리스트에 추가합니다. 

 윈도우10 에서 해당 구현을 따라가보면 내부적으로 SleepConditionVariableSRW으로 구현되어 있습니다. 

<예제>

std::mutex				m_;
std::condition_variable		cv_;
std::queue<std::string>		con_;

// queue에서 데이터를 추출하고, queue가 비어 있다면 condition_variable을 통해서 대기 합니다.                            
bool Pop(std::string& obj) {
    std::unique_lock<std::mutex> ul(m_);
    if (con_.empty())
    {
        cv_.wait(ul, [] {return !con_.empty(); });
    }

    obj = con_.front(); con_.pop();
    return true;
}

 

wait_for 함수

template <class _Rep, class _Period>
cv_status wait_for(unique_lock<mutex>& _Lck, const chrono::duration<_Rep, _Period>& _Rel_time) {
    
template <class _Rep, class _Period, class _Predicate>
bool wait_for(unique_lock<mutex>& _Lck, const chrono::duration<_Rep, _Period>& _Rel_time, _Predicate _Pred) {

 wait함수와 기본적인 동작은 동일하며 추가적으로 시간을 입력받아서 time out을 발생시킵니다. 

 std::chrono를 사용해서 기간을 입력받으며 입력 받은 기간이 지날 때까지 통지가 없다면 timeout을 발생시키고

 스레드의 wait를 중단합니다.

 스레드 스케줄링에 의해서 입력한 시간보다 더 오래 대기 할 수도 있습니다.

<예제>

using namespace std::chrono_literals;
// wait_for를 통해서 10초 이상 기다리면 timeout을 발생합니다. 
auto status = cv_.wait_for(ul, 10s, [] {return !con_.empty(); });
if(!status){
	// cv_status::timeout
}

 

wait_until 함수

template <class _Clock, class _Duration>
cv_status wait_until(unique_lock<mutex>& _Lck, const chrono::time_point<_Clock, _Duration>& _Abs_time);

template <class _Clock, class _Duration, class _Predicate>
bool wait_until(unique_lock<mutex>& _Lck, const chrono::time_point<_Clock, _Duration>& _Abs_time, _Predicate _Pred) {

 wait_for와 기본적으로 동일하게 작동합니다. wait_for 다른 점은 기간이 아니고 특정 시점의 시간을 지정해서 

 특정 시점까지 통지를 받지 못하면 timeout이 발생합니다. 

<예제>

using namespace std::chrono_literals;
auto now = std::chrono::system_clock::now();
// wait_until를 통해서 지정된 시간까지 통지를 못받으면 timeout을 발생합니다. 
auto status = cv_.wait_until(ul, now + 10s);
if (status == std::cv_status::timeout) {
	// cv_status::timeout
	return false;
}

 

spurious wakeups

 std::condition_variale에 대해서 공부하다 보면 "spurious wakeups" 단어가 나오게 되는데 어떤 뜻인지 확인해보고

 어떻게 발생하는지 확인해봅니다. 

 아래는 위키피디아에 올라온 "spurious wakeups"의 설명입니다. 링크  

 spurious wakeups란 비정상적으로 깨어난 조건 변수 알림을 뜻합니다.

 그렇다면 왜 이런 현상이 발생할까요?

 일반적으로 condition_variable을 통해서 notify 함수를 호출 할 때 mutex의 잠금을 유지할 필요가 없습니다. 

 그렇기 때문에 아래와 같은 시나리오가 발생할 수 있습니다. 

  1. 스레드 A가 condition variable을 통해서 스레드에 안전한 queue에 데이터가 들어오길 대기합니다.
  2. 스레드 B가 queue에 데이터를 추가한 후에 mutex를 해제 한 후에 통지 알림을 보내기 전에 컨텍스트 스위치가 발생합니다. 
  3. 스레드 C가 queue에 데이터를 추가한 후에 통지 알림을 보냅니다. 
  4. 스레드 A는 통지를 전달받고 queue에 있는 2개의 데이터를 모두 처리 합니다.
  5. 스레드 B가 다시 시작되고 통지 알림을 전달합니다
  6. 스레드 A는 깨어나지만 큐가 비어 있는 상태입니다. (spurious wakeups 발생합니다.)

 다양한 이유로 해당 현상이 발생할 수 있습니다.

 정확한 통지 전달을 위해서 잠금을 유지한 상태로 통지하고 잠금을 해제 할 수도 있습니다. 

 하지만 이 방법은 통지를 전달 받은 스레드가 잠금이 해제 될 때까지 대기 할 수 있기 때문에 성능이

 떨어집니다. 

참 조 

윈도우에서 condition variable 구현

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

 

[동기화객체] 조건 변수(Condition Variable)

조건 변수 사용 방법  조건 변수는 어떠한 상황에서 사용할 수 있을까요? 예를 들면 로그를 처리하기 위한 생산자 스레드와 소비자 스레드가  있다고 가정합니다. 생산자 스레드에서는 로그 데

jungwoong.tistory.com

c++ reference 문서 

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

 

std::condition_variable - cppreference.com

class condition_variable; (since C++11) The condition_variable class is a synchronization primitive that can be used to block a thread, or multiple threads at the same time, until another thread both modifies a shared variable (the condition), and notifies

en.cppreference.com

https://docs.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-wakeconditionvariable

 

WakeConditionVariable function (synchapi.h) - Win32 apps

Wake a single thread waiting on the specified condition variable.

docs.microsoft.com

https://stackoverflow.com/questions/6419117/signal-and-unlock-order

 

signal and unlock order

void WorkHandler::addWork(Work* w){ printf("WorkHandler::insertWork Thread, insertWork locking \n"); lock(); printf("WorkHandler::insertWork Locked, and inserting into queue \n");

stackoverflow.com

 

'c++ > modern c++' 카테고리의 다른 글

[c++20] module(모듈)  (0) 2022.06.13
[c++20] Concept  (0) 2022.06.10
[c++] std::lock 관련 함수들  (0) 2020.05.22
[c++] std::mutex  (0) 2020.03.28
[c++17] string_view  (0) 2020.03.21

+ Recent posts