조건 변수(Condition Variable)
이전 글에서 멀티쓰레드 이벤트 LOCK 에 대해 알아봤다.
https://ddukddaksudal.tistory.com/62
멀티 쓰레드 Lock 구현
Lock 상태일 때 다른 쓰레드는 뭘 할까? 어떤 쓰레드가 lock을 하고 메모리를 차지했다면 다른 쓰레드들은 unlock될 때 까지 기다려야 한다. 그런데 기다린다는 것은 말하자면 시간 낭비이다. 어떻게
ddukddaksudal.tistory.com
그런데 이러면 이벤트와 lock 코드가 분리되어 있기 때문에 쓰레드가 원하는대로 동작하지 않을 수 있다. 예를 들어 handle이 signal 상태가 되어 WaitForSingleObject함수를 탈출하고 lock을 얻으려고 했는데 그 사이에 다시 다른 쓰레드가 lock을 걸어버릴 수 있다는 것이다.
이 문제를 해결하려면 WaitForSingleObject함수와 Lock을 atomic 하게 만들어줄 필요가 있다. 그것을 해주는 것이 바로 조건 변수 이다. 즉 조건 변수는 이벤트 Lock의 조금 더 발전된 형태라고 할 수 있다.
condition_variable
조건 변수를 구현한 클래스 이다. condition_variable은 표준 라이브러리에 들어있따. #include <condition_variable> 을 통해 사용할 수 있다.
조건 변수는 작동 방식이 이벤트와 비슷하다. 그러나 이벤트와 달리 유저 레벨 오브젝트이다. 커널 오브젝트가 아니다. 따라서 이벤트보다 가볍게 동작한다. 하지만 이벤트처럼 다른 프로세스와 통신한다던가 할 수는 없다. 그러므로 하나의 프로그램 내에서만 쓸거면 이벤트 보다 조건 변수가 훨씬 좋은 선택일 것이다.
condition_variable의 쓰레드 별 동작.
condition_variable사용 방법을 알아보자.
- 통지 쓰레드 t1 :
- lcok을 잡음
- 공유변수 값 수정
- lock을 풀고
- 조건변수를 통해 다른 쓰레드에게 통지
- 대기 쓰레드 t2 :
- lock을 잡음
- 조건 확인
- 조건만족-> 빠져나와 다음 코드 진행
- 조건불만족-> lock을 풀고 대기상태로 전환
condition_variable 구문
위에 설명한 동작들을 구현하기 위한 구문들을 알아보자.
- #include <condition_variable>
- condition_variable cv; // 선언
- condition_variable_any cv; // 선언2 아래에 설명함.
- cv.notify_one(); // 대기중인 쓰레드를 하나 깨움.
- cv.notify_all(); // 대기중인 쓰레드를 모두 깨움.
- cv.wait(lock, [](){탈출 조건}) // 탈출 조건 만족하면 빠져나옴. 불만족하면 lock 풀고 대기. unique_lock만 사용가능.
- 그 외 waitfor, waitunitl 등
condition_variable_any
기본적인 동작은 condition_variable과 같다. 그러나 condition_variable은 unique_lock만 사용 가능한 것과 달리 condition_variable_any는 unique_lock이 아닌 BasicLockable 객체로 사용할 수 있다.
unique_lock만 사용 가능한 이유
condition_variable의 내부에서 lock을 또 호출해주기 때문. lock_guard는 무조건 생성자에서 lock을 이미 해주었기 때문에 unique_lock만 가능하다. 통지 쓰레드에서는 lock_guard를 사용해도 괜찮지만, wait 함수를 사용하는 대기 쓰레드에서는 unique_lock만 사용가능하다.
실제 구현 코드
그럼 이전에 공부한 이벤트 Lock 대신 conditon_variable을 사용해 개선한 코드를 보자.
queue<int> que;
mutex m;
HANDLE handle;
condition_variable cv;
void pushThread()
{
while (true)
{
{
unique_lock<mutex> lg(m);
que.push(100);
}
cv.notify_one();
this_thread::sleep_for(10000ms);
}
}
void popThread()
{
while (true)
{
unique_lock<mutex> lg(m);
cv.wait(lg, []() {return que.empty() == false; });
que.pop();
cout << que.size() << endl;
}
}
int main()
{
thread t1(pushThread);
thread t2(popThread);
t1.join();
t2.join();
CloseHandle(handle);
return 0;
}
Spurious WakeUp(가짜 기상) 문제
notify_one과 공유변수를 수정해주는 부분이 분리되어있기 때문에 그 사이에 공유변수가 수정될 수 있다. 그러면 대기 스레드가 깨어났는데도 조건을 만족하지 않는 상태가 돼버린다. 이 문제를 해결하기 위해서는 대기 쓰레드에서 조건을 다시 체크해줘야 한다.