728x90
멀티스레드 환경에서 스레드 간의 협업을 설계할 때, 무작정 기다리는 것(Busy-wait)은 자원 낭비입니다.
유저 레벨에서 효율적으로 스레드 간 신호를 주고받을 수 있는 Condition Variable(조건 변수)가 있습니다.
Condition Variable은 유저 레벨 오브젝트(User-Level Object)입니다.
- 가벼운 동기화: 커널 오브젝트(Event, Semaphore 등)가 프로세스 간(Inter-process) 동기화가 가능해 무거운 커널 모드 전환을 동반하는 반면, Condition Variable은 동일한 프로세스 내 스레드 간 동기화에 최적화되어 있어 상대적으로 가볍고 빠릅니다.
- 배타적 제어와 신호: 단순히 "기다려라"가 아니라, 특정 "조건"이 만족될 때까지 스레드를 효율적으로 휴식 상태로 만듭니다.
가장 표준적인 큐(Queue) 기반의 생산자-소비자 모델입니다.
queue<int32> q;
mutex m;
condition_variable cv;
void Producer()
{
while (true)
{
{
unique_lock<mutex> lock(m);
q.push(1);
}
cv.notify_one();
}
}
void Consumer()
{
while (true)
{
unique_lock<mutex> lock(m);
// lock 잡고, 조건 확인
// 1) 조건이 만족 시, 이어서 진행
// 2) 조건을 만족하지 않는다면, lock을 풀고 wait
// cv에서 unique_lock을 받는 이유는 경우에 따라 풀어줘야 하기 때문
cv.wait(lock, []() {return q.empty() == false; });
{
int32 data = q.front();
q.pop();
}
}
}
코드를 보면 lock_guard가 아닌 unique_lock을 사용합니다.
그 이유는 Condition Variable의 작동 방식 때문입니다.
1) 대기 상태에 들어갈 때 락을 해제(Unlock)하여 다른 스레드(Producer)가 접근할 길을 열어줍니다.
2) 신호를 받고 깨어날 때 다시 락을 획득(Lock)하여 안전하게 자원을 점유합니다.
이렇게 자유자재로 락을 걸고 풀 수 있는 유연함이 필요하기 때문에 unique_lock이 필수적입니다.
* 로직
- 생산자(Producer): 락을 잡고 큐에 데이터를 안전하게 push한 뒤 락을 해제합니다. 이후 notify_one()을 통해 잠들어 있는 소비자에게 신호를 보냅니다.
- 소비자(Consumer) 대기: wait 함수에 진입합니다. 만약 큐가 비어있다면, 잡고 있던 락을 스스로 풀고 대기 모드로 들어갑니다.
- 소비자 재개: 생산자의 신호를 받으면 소비자가 깨어납니다. 이때 자동으로 다시 락을 획득한 뒤 조건을 검사합니다.
- 조건 확인: q.empty() == false가 참이라면 비로소 아래의 팝(Pop) 로직을 수행합니다.
728x90