멀티스레드와 동시성

Ch08. 생산자 소비자 문제 - wait, notify의 한계

webmaster 2024. 8. 23. 23:57
728x90

지금까지 살펴본 Object.wait() , Object.notify() 방식은 스레드 대기 집합 하나에 생산자, 소비자 스레드를 모두 관리한다.그리고 notify() 를 호출할 임의의 스레드가 선택된다.
따라서 앞서 살펴본 것 처럼 큐에 데이터가 없는 상황에 소비자가 같은 소비자를 깨우는 비효율이 발생할 있다.
또는 큐에 데이터가 가득 차있는데 생산자가 같 은 생산자를 깨우는 비효율도 발생할 있다.

 

비효율 생산자 실행

초기 상태

  • 다음과 같은 상황을 가정하겠다.
  • 큐에 "dataX"가 보관되어 있다.
  • 스레드 대기 집합에는 다음 스레드가 대기하고 있다.
    • 소비자: c1 , c2 , c3
    • 생산자: p1 , p2 , p3
  • p0 스레드가 data0 생산을 시도한다.

결과 분석 - 시간의 흐름 1

  • p0 스레드가 실행되면서 data0를 큐에 저장한다. 이때 큐에 데이터가 가득 찬다.
  • notify()를 통해 대기 집합의 스레드를 하나 깨운다.

결과 분석 - 시간의 흐름 2

  • 만약 notify()의 결과로 소비자 스레드가 깨어나게 되면 소비자 스레드는 큐의 데이터를 획득하고, 완료된다.

결과 분석 - 시간의 흐름 3

  • 만약 notify() 의 결과로 생산자 스레드를 깨우게 되면, 이미 큐에 데이터는 가득 차 있다. 따라서 데이터를 생 \산하지 못하고 다시 대기 집합으로 이동하는 비효율이 발생한다.

비효율 소비자 실행

초기 상태
결과 분석 - 시간의 흐름 1

  • c0 스레드가 실행되고 data0를 획득한다.
  • 이제 큐에 데이터는 비어있게 된다.
  • c0 스레드는 notify()를 호출한다.

결과 분석 - 시간의 흐름 2

  • 스레드 대기 집합에서 소비자 스레드가 깨어나면 큐에 데이터가 없기 때문에 다시 대기 집합으로 이동하는 비효율이 발생한다.

결과 분석 - 시간의 흐름 3

  • 스레드 대기 집합에서 생산자 스레드가 깨어나면 큐에 데이터를 저장하고 완료된다.

같은 종류의 스레드를 깨울 비효율이 발생한다.
내용을 통해서 있는 사실은 생산자가 같은 생산자를 깨우거나, 소비자가 같은 소비자를 깨울 비효율이 발생 할 있다는 점이다. 생산자가 소비자를 깨우고, 반대로 소비자가 생산자를 깨운다면 이런 비효율은 발생하지 않는다.

 

스레드 기아

notify()의 또 다른 문제점으로는 어떤 스레드가 깨어날지 알 수 없기 때문에 발생할 수 있는 스레드 기아 문제가 있다.

스레드 기아 문제

  • notify()가 어떤 스레드를 깨울지는 알 수 없다. 최악의 경우 c1 ~ c5 스레드가 반복해서 깨어날 수 있다.
    • c1 이 깨어나도 큐에 소비할 데이터가 없다. 따라서 다시 스레드 대기 집합에 들어간다.
    • notify()로 다시 깨우는데 어떤 스레드를 깨울지 알 수 없다. 따라서 c1 ~ c5 스레드가 반복해서 깨어날 수 있다.
  • p1 은 실행 순서를 얻지 못하다가 아주 나중에 깨어날 수도 있다.
  • 이렇게 대기 상태의 스레드가 실행 순서를 계속 얻지 못해서 실행되지 않는 상황을 스레드 기아(starvation) 상태라 한다.
  • 물론 p1이 가장 먼저 실행될 수도 있다.
  • 이런 문제를 해결하는 방법 중에 notify() 대신에 notifyAll() 사용하는 방법이 있다.

notifyAll()

notifyAll()을사용하면 스레드 대기 집합에 있는 모든 스레드를 한번에 깨울 있다.

notifyAll() 초기 상태

  • 데이터를 획득한 c0 스레드가 notifyAll()을 호출한다.

notifyAll() 결과 분석 - 시간의 흐름 1

 

  • 대기 집합에 있는 모든 스레드가 깨어난다.
  • 모든 스레드는 다 임계 영역 안에 있다. 따라서 락을 먼저 획득해야 한다.
  • 락을 획득하지 못하면 "BLOCKED" 상태가 된다.
  • 만약 c1 이 먼저 락을 먼저 획득한다면 큐에 데이터가 없으므로 다시 스레드 대기 집합에 들어간다.
  • c2 ~ c5 모두 마찬가지이다.

notifyAll() 결과 분석 - 시간의 흐름 2

  • 따라서 p1 이 가장 늦게 락 획득을 시도해도, c1 ~ c5는 모두 스레드 대기 집합에 들어갔으므로 결과적으로 "p1"만 남게 되고, 결국 락을 획득하게 된다.(스레드 기아 문제 해결)

notifyAll() 결과 분석 - 시간의 흐름 3

  • p1 은 락을 획득하고, 데이터를 생성한 다음에 notifyAll()을 호출하고 실행을 완료할 수 있다.
  • 참고로 반대의 경우도 같은 스레드 기아 문제가 발생할 수 있기 때문에 notifyAll() 을 호출한다.

결과적으로 notifyAll()을사용해서 스레드 기아 문제는 막을 있지만, 비효율을 막지는 못한다.

728x90