조건 변수: 기다릴 때 잠금을 놓고 깨어나면 다시 확인하는 이유
공유 큐가 비어 있을 때 소비자가 계속 빈 상태를 검사하면 CPU를 낭비한다. 잠금을 잡은 채 잠들면 생산자가 큐에 항목을 넣지 못한다. 조건 변수는 공유 상태가 바뀔 때까지 효율적으로 기다리는 장치다. 대기는 뮤텍스와 함께 사용하며, 기다리는 동안 뮤텍스를 놓고 깨어날 때 다시 얻는 규약이 핵심이다.
조건 변수의 알림 뒤에는 보호된 상태를 반드시 다시 확인한다.
기다리는 것은 알림보다 상태
소비자는 먼저 뮤텍스를 얻고 큐가 비어 있는지 확인한다. 비었다면 조건 변수의 wait를 호출한다. 이 동작은 잠금 해제와 대기 등록을 관련 규약에 따라 원자적으로 연결해, 생산자의 상태 변경과 알림 사이에서 깨어남을 잃지 않게 한다. 소비자가 깨어나 wait에서 돌아올 때는 뮤텍스를 다시 가진다.
생산자는 같은 뮤텍스로 큐를 보호하며 항목을 추가한다. 이후 필요한 대기자를 깨우는 알림을 보낸다. 생산자가 언제 알림을 보내야 하는지에 관한 세부 선택은 API와 프로그램 설계를 따라야 하지만, 공유 상태 변경은 대기자의 검사와 같은 동기화 규칙을 따라야 한다. 상태를 잠금 밖에서 바꾸면 확인과 대기 사이의 틈이 다시 생길 수 있다.
if가 아니라 while인 까닭
소비자가 깨어났다고 반드시 항목을 가져갈 수 있는 것은 아니다. 여러 소비자 중 다른 소비자가 먼저 항목을 가져갔거나, 허용된 가짜 깨어남이 발생할 수 있다. 그래서 while queue_is_empty: wait(...)처럼 조건을 재검사한다. POSIX 조건 변수 대기 규정은 복귀 후에도 조건을 다시 평가해야 한다고 명시한다.
예를 들어 항목 하나를 넣고 여러 소비자를 깨웠다면, 첫 소비자가 항목을 가져간 뒤 두 번째 소비자는 빈 큐를 보게 된다. 두 번째도 알림만 믿고 꺼내면 오류다. while 검사에서는 다시 잠들 수 있다. 알림은 상태를 볼 기회를 주는 것이고, 상태가 지금 참이라는 보증은 아니라는 차이를 기억하면 이해가 쉽다.
알림 자체를 저장하지 않는다
조건 변수는 보통 세마포어처럼 사용 가능한 허용량을 누적하지 않는다. 아무도 기다리지 않을 때 보낸 알림을 미래 대기자가 자동으로 받는다고 가정해서는 안 된다. 지속해야 하는 사실은 큐의 항목, 종료 플래그, 작업 완료 수 같은 공유 상태로 저장한다. 새로 도착한 소비자는 이 상태를 먼저 검사하므로 이미 준비된 작업을 놓치지 않는다.
한 대기자를 깨우는 signal과 여러 대기자를 깨우는 broadcast는 다른 도구다. 상태 변화가 한 작업만 진행시킨다면 하나를 깨우는 편이 불필요한 경쟁을 줄일 수 있다. 모든 작업이 종료 플래그를 확인해야 한다면 전체 알림이 필요할 수 있다. 전체 알림은 깨어난 스레드들이 같은 잠금을 얻으려 경쟁하는 비용도 만든다.
시간 제한 대기에서는 시간 만료와 상태 변화가 가까운 시점에 일어날 수 있어 반환값과 공유 조건을 모두 해석해야 한다. 프로그램 종료 시 대기자를 남겨 두지 않는 규칙도 필요하다. 조건 변수의 올바른 설명은 항상 보호할 상태, 뮤텍스, 반복 검사, 상태를 바꾸는 작업, 알림의 범위를 함께 포함한다. 단순한 잠들기 함수로 취급하면 가장 중요한 합의가 빠진다.