CodeNote
Coding Note
CodeNote
전체 방문자
오늘
어제
  • 전체 보기 (35)
    • C++ (33)
      • Modern C++ (12)
      • Modern C++ STL (0)
      • Thread (16)
      • Thread (Async) (5)
    • 디자인패턴 (0)
    • Algorithm (2)
    • Electron (0)
    • Python (0)

블로그 메뉴

  • 홈
  • Github
  • 태그
  • 방명록

공지사항

인기 글

태그

  • LOCK
  • Free
  • C++ #Memory
  • 자료구조
  • mutex
  • C++

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
CodeNote
C++/Thread

[Thread] Condition Variable

[Thread] Condition Variable
C++/Thread

[Thread] Condition Variable

2022. 7. 23. 22:44

Condition Variable

Concurrency 프로그래밍을 한다면 필수적으로 알아야하는 개념이다. Condition Variable을 사용하는 목적은 하나의 Thread에서 다른 Thread로 신호를 보낼 때 사용한다.

 

자세히 알아보기 위해 그림을 보면,

 

실행 과정

  1. Wait Thread는 진행 과정 중에 condition variable의 wait() 함수를 만나면서 block 상태가 된다.
  2. 다른 Thread에서 두 Thread간의 공유되는 변수 condition(var)의 값을 수정하고 condition variable을 통해서 signal을 보낸다.
  3. block 상태에 있던 Wait Thread는 신호를 받고 var를 체크한 뒤 조건이 만족되면 해당 Thread를 계속 진행한다.

여기서 신호(signal)는 notify_one() 함수를 통해 줄 수 있다.

Example Code : 잘못된 사용

#include <iostream>
#include <thread>
#include <condition_variable>
#include <chrono>
#include <mutex>

std::mutex mtx;
std::condition_variable cv;

void waitFn()
{
    std::cout << "wait" << std::endl;
    std::unique_lock<std::mutex> lck(mtx);
    cv.wait(lck); // argument로 유니크 락이 필요하다.
    std::cout << "re run" << std::endl;
}

void signalFn()
{
    std::cout << "signal" << std::endl;
    cv.notify_one();
}

int main()
{
    std::thread waitT(waitFn);
    std::thread signalT(signalFn);

    waitT.join();
    signalT.join();
    
    return 0;
}
  • 참고로 위의 코드는 완벽하게 틀린 코드이다. 

왜 틀린 코드인 것일까?

출력 결과 1

출력 결과 1은 wait, signal, re run이 순차적으로 출력이 되었으므로 정상 동작한다고 생각할 수 있다.

출력 결과 2

몇 번 반복해서 실행했더니 출력 결과 2와 같이 wait signal은 출력 되었으나 무한 대기상태가 되었다. 바로 이러한 문제 때문에 위의 코드는 틀린 코드이며 condition variable을 사용할 때는 mutex와 shared variable이 필요한 것이다.

 

출력 결과 2의 원인은 Lost Wakeup 때문이다. wait thread가 wait함수를 통해 대기 상태로 넘어가기 전에 signal thread에서 먼저 신호를 보낸다면 해당 signal은 wait thread가 block 상태가 되기전에 도착하였기 때문에 wait thread는 영원한 block 상태에 빠지게 된다.

 

이 문제를 해결하기 위해서 두 thread간의 공유가되는 condition(shared variable)을 사용하는 것이고 이 shared variable을 여러 thread의 접근으로부터 보호하기 위해서 mutex를 사용한다. 이처럼 복잡한 메커니즘이 필요한 또다른 이유로는 spurious wakeup을 방지하기 위해서이다. (spurious wakeup의 자세한 내용은 검색해보자.)

 

Example Code : 올바른 사용

#include <iostream>
#include <thread>
#include <condition_variable>
#include <chrono>
#include <mutex>

bool readyFlag = false; // 두 thread에서 동시 접근하므로 mutex로 보호해야한다.
std::mutex mtx;
std::condition_variable cv;

void waitFn()
{
    std::cout << "wait" << std::endl;
    std::unique_lock<std::mutex> lck(mtx);
    // readyFlag가 false일 때만 대기 상태로 빠지면 된다.
    while (!readyFlag)
    {
        cv.wait(lck); // lock released
    }
    // while문을 사용하기 싫다면 다음과 같이 람다 함수를 사용해도 된다.
    // cv.wait(lck, [] {return readyFlag;});
    
    // critical section
    // 크리티컬 섹션 구간을 타이트하게 가져가고 싶다면 명시적으로 unlock()해도 된다.
    lck.unlock();

    std::cout << "re run" << std::endl;
}

void signalFn()
{
    std::cout << "signal" << std::endl;
    // notify_one까지는 보호할 필요없으므로 scope로 제한
    {
        std::lock_guard<std::mutex> lck(mtx);
        readyFlag = true;
    }
    cv.notify_one();
}

int main()
{
    std::thread waitT(waitFn);
    std::thread signalT(signalFn);

    waitT.join();
    signalT.join();
    
    return 0;
}

먼저, waitFn을 살펴보면 처음에 mutex lock을 걸고 readyFlag를 읽는다. 만약 readyFlag가 준비 되어있지 않다면 해당 thread는 cv.wait() 함수를 통해 block이 되고 lock은 해제된다. 이는 당연한 과정인데 block 상태로 빠지면서 lock을 해제하지 않으면 해당 mutex는 어떠한 thread도 재획득할 수 없기 때문이다. 이어서 block 상태에 있던 thread가 signal을 받으면 mutex lock을 획득한 후 readyFlag를 체크하고 조건이 만족되면 while loop를 벗어난 후 다음 코드를 진행한다.

 

또한, 이렇게 복잡한 메커니즘을 갖는 이유 중 하나로 가짜 웨이크업(suprious wakeup)이 있을 수 있다고 하였는데 만약 가짜 웨이크업이 일어난다면 block 되어있던 Wait Thread가 깨어날 수 있다. 그 때는 해당 Thread가 깨어나더라도 readyFlag가 true로 Set이 되어있지 않기 때문에 다시 wait 상태로 돌아갈 수 있다.


signlFn을 살펴보면 lock_guard를 사용해서 scope를 제한하였는데 unique_lock을 사용해도 된다. 그리고 notify_one() 호출전에 unlock() 해주면 된다.

void signalFn()
{
    std::cout << "signal" << std::endl;
    // unique_lock 사용
    std::unique_lock<std::mutex> lck(mtx);
    readyFlag = true;
    lck.unlock();

    cv.notify_one();
}

또한, 지금은 notify_one()을 이용해서 signal을 하나만 보내는데 condition_variable에는 wait 중인 thread들을 모두 깨우는 notify_all()이라는 함수도 있다. 물론, 모든 thread들이 동시에 깨어난다하더라도 깨어난 thread가 mutex lock, condition 체크를 진행하고 크리티컬 섹션까지는 오직 하나의 thread만 가능하다는 것을 잊으면 안된다.

 

 

'C++ > Thread' 카테고리의 다른 글

[Thread]semaphore  (0) 2022.07.26
[Thread] Producer-Consumer 패턴  (0) 2022.07.26
[Thread] scoped static 초기화  (0) 2022.07.19
[Thread] std::call_once  (0) 2022.07.18
[Thread] std::shared_mutex  (0) 2022.07.17
  • Condition Variable
'C++/Thread' 카테고리의 다른 글
  • [Thread]semaphore
  • [Thread] Producer-Consumer 패턴
  • [Thread] scoped static 초기화
  • [Thread] std::call_once
CodeNote
CodeNote
기록 블로그

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.