C++

조건 변수(Condition Variable)

춤추는수달 2022. 1. 27. 07:46

이전 글에서 멀티쓰레드 이벤트 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 :
    1. lcok을 잡음
    2. 공유변수 값 수정
    3. lock을 풀고
    4. 조건변수를 통해 다른 쓰레드에게 통지
  • 대기 쓰레드 t2 :
    1. lock을 잡음
    2. 조건 확인
    3. 조건만족-> 빠져나와 다음 코드 진행
    4. 조건불만족-> 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과 공유변수를 수정해주는 부분이 분리되어있기 때문에 그 사이에 공유변수가 수정될 수 있다. 그러면 대기 스레드가 깨어났는데도 조건을 만족하지 않는 상태가 돼버린다.  이 문제를 해결하기 위해서는 대기 쓰레드에서 조건을 다시 체크해줘야 한다.