← 문제로

25. 작업 큐 워커의 조건 대기 술어 재확인 부재 (spurious/다중 소비 wakeup)

난이도 중
내 리뷰 · C#
해설 · C#

해설 — 작업 큐 워커의 조건 대기 술어 재확인 부재 (spurious/다중 소비 wakeup)

난이도: 중

요약

워커가 큐가 비었는지를 if로 한 번만 확인하고 Monitor.Wait(C) 한 뒤, 깨어나면 무조건 _q.Dequeue()(D) 한다. 하지만 "깨어남 == 처리할 작업이 있음"이 아니다. 가짜 깨어남(spurious wakeup) 이나, 여러 워커가 같은 Pulse에 경쟁해 한쪽이 이미 큐를 비운 경우, 깨어난 워커가 빈 큐에서 DequeueInvalidOperationException을 던진다. 조건 대기는 반드시 술어를 while로 재확인해야 한다.

문제점

1. (조건 대기) 술어를 while가 아니라 if로 확인 — (B)(C)(D)

  • 증상: 워커가 _q.Dequeue()(D)에서 InvalidOperationException("Queue empty")로 죽거나, 작업을 처리하지 못한다.
  • 재현조건:
    • 다중 워커: 워커 둘이 Wait 중인데 Enqueue가 작업 1개 + Pulse(하나만 깨움) → 깨어난 워커가 처리. 그런데 또 다른 경로로 둘 다 깨어나거나, PulseAll 류로 둘 다 깨면 한 워커가 먼저 Dequeue해 비우고, 다음 워커가 빈 큐에서 Dequeue.
    • 가짜 깨어남: Monitor.WaitPulse 없이도 깨어날 수 있다(스펙상 허용). if라 술어를 다시 안 보고 바로 Dequeue.
  • 근본원인: "깨어남"은 "조건 충족"의 보장이 아니라 "조건을 다시 확인하라"는 신호일 뿐이다. if는 깨어난 직후 한 번도 재확인하지 않는다.

2. (확장성) 다중 소비자에서 깨어남-경쟁

  • 워커가 여러 개면 Pulse 하나에 여러 워커가 순차로 락을 잡고 깨어나며, 작업 수보다 깨어난 워커가 많으면 잉여 워커는 빈 큐를 본다. if는 이 경우를 처리 못 한다.

3. (종료 불가) 깔끔한 종료 경로 없음

  • 무한 while(true)에 종료 신호가 없어 셧다운 시 워커를 멈출 방법이 없다(드레이닝/조인 불가).

수정안

핵심은 술어를 while로 재확인하는 것이다(+ 종료 신호).

private bool _closing = false;

public void WorkerLoop()
{
    while (true)
    {
        Job job;
        lock (_lock)
        {
            while (_q.Count == 0 && !_closing)   // 깨어날 때마다 술어 재확인
                Monitor.Wait(_lock);
            if (_closing && _q.Count == 0) return; // 종료 경로
            job = _q.Dequeue();
        }
        job.Run();   // 처리는 락 밖에서
    }
}

public void Close()
{
    lock (_lock) { _closing = true; Monitor.PulseAll(_lock); }
}
  • while이라 가짜 깨어남·다중 소비 경쟁에도 빈 큐를 다시 보고 안전하게 재대기한다.
  • 종료 시 PulseAll로 모든 워커를 깨워 드레이닝 후 종료.
  • job.Run()락 밖에서 — 처리 동안 큐를 막지 않는다(처리량).

더 나은 설계

  • BlockingCollection<T>/Channel<T>: 표준 동시성 큐가 조건 대기·종료(complete)·다중 소비자를 안전하게 캡슐화한다. 직접 Monitor 구현보다 버그가 적다.
  • 종료 토큰: CancellationToken으로 드레이닝·취소를 일급으로.
  • 트레이드오프: 직접 Monitor는 의존성이 없지만 술어 재확인·종료·예외 처리를 모두 손수 맞춰야 한다. 라이브러리 큐는 안전하지만 동작/할당 특성을 이해하고 써야 한다.

면접 포인트

  1. 조건 대기는 항상 while(술어 재확인)Monitor.Wait/cv.wait는 가짜 깨어남이 허용되고, 다중 소비자에선 깨어남이 곧 조건 충족이 아니다.
  2. "깨어남 = 조건을 다시 검사하라는 신호"라는 멘탈 모델. if는 거의 항상 버그.
  3. 실무에선 BlockingCollection/Channel로 바퀴 재발명을 피하고, 종료/드레이닝 경로를 처음부터 설계.