멀티스레드와 동시성

Ch08. 생산자 소비자 문제 - 예제2(생산자, 소비자 대기)

webmaster 2024. 8. 21. 23:13
728x90

실행 코드 - 생산자, 소비자 대기

  • put(data): 데이터를 버리지 않는 대안
    • data3을 버리지 않는 대안은, 큐가 가득 찾을 때, 큐에 빈 공간이 생길 때까지, 생산자 스레드가 기다리면 된다. 언젠가는 소비자 스레드가 실행되어서 큐의 데이터를 가져갈 것이고, 그러면 큐에 데이터를 넣을 수 있는 공간이 생기게 된다.
    • 여기서는 생산자 스레드가 반복문을 사용해서 큐에 빈 공간이 생기는지 주기적으로 체크한다. 만약 빈 공간이 없다면 sleep()을 사용해서 잠시 대기하고, 깨어난 다음에 다시 반복문에서 큐의 빈 공간을 체크하는 식으로 구현했다.
  • take(): 큐에 데이터가 없다면 기다리자
    • 소비자 입장에서 큐에 데이터가 없다면 기다리는 것도 대안이다. 큐에 데이터가 없을 때 null을 받지 않는 대안은, 큐에 데이터가 추가될 때까지 소비자 스레드가 기다리는 것이다. 언젠가는 생산자 스레드가 실행되어서 큐의 데이터를 추가할 것이고, 큐에 데이터가 생기게 된다. 물론 생산자 스레드가 계속해서 데이터를 생산한다는 가정이 필요하다.
    • 여기서는 소비자 스레드가 반복문을 사용해서 큐에 데이터가 있는지 주기적으로 체크한 다음에, 만약 데이터가 없다면 sleep() 사용해서 잠시 대기하고, 깨어난 다음에 다시 반복문에서 큐에 데이터가 있는지 체크하는 식으로 구현했다.

실행 코드 - Main

  • BoundedQueueV2를 사용하도록 변경
  • 생산자 먼저 실행하도록 주석 변경

실행 결과 - 생산자 먼저 실행

  • producer3 이 종료되지 않고 계속 수행되고, consumer1 , consumer2 , consumer3 은 "BLOCKED" 상태가 된다.

참고: 만약 실행 결과가 지금 내용과 다르고 특히 "현재 상태 출력"과 그 이후 부분이 나오지 않는다면 toString()에 있는 synchronized를 제거해야 한다. 원칙적으로 toString() 에도 synchronized 를 적용해야 한다. 그래야 toString() 을 통한 조회 시점에도 모니터 락이 걸리며 정확한 데이터를 조회할 수 있다. 하지만 이 부분이 이번 설명의 핵심이 아니고, 예제 코드를 단순하게 유지하기 위해 여기서는 toString() synchronized 사용하지 않겠다. 결과에 차이가 나는지는 이후에 설명하는 내용을 들어보면 자연스럽게 이해가 것이다.

실행 결과 - 소비자 먼저 실행

  • 소비자 먼저 실행의 경우 consumer1 이 종료되지 않고 계속 수행된다. 그리고 나머지 모든 스레드가 "BLOCKED" 상태가 된다.

생산자 스레드 -> 소비자 스레드 실행 결과 분석

초기 상태

생산자 스레드 실행 시작

실행 결과 분석 - 시간의 흐름1
23:44:04.108 [     main] == [생산자 먼저 실행] 시작, BoundedQueueV2 == 

23:44:04.113 [     main] 생산자 시작
23:44:04.141 [producer1] [생산 시도] data1 -> []

실행 결과 분석 - 시간의 흐름2

23:44:04.142 [producer1] [생산 완료] data1 -> [data1]

실행 결과 분석 - 시간의 흐름3

23:44:04.229 [producer2] [생산 시도] data2 -> [data1]

실행 결과 분석 - 시간의 흐름4

23:44:04.229 [producer2] [생산 완료] data2 -> [data1, data2]

실행 결과 분석 - 시간의 흐름5

23:44:04.334 [producer3] [생산 시도] data3 -> [data1, data2]
23:44:04.334 [producer3] [put] 큐가 가득 참, 생산자 대기
  • 생산자 스레드인 p3는 임계 영역에 들어가기 위해 먼저 락을 획득한다.
  • 큐에 data3을 저장하려고 시도한다. 그런데 큐가 가득 차있다.
  • p3는 sleep(1000)을 사용해서 잠시 대기한다. 이때 "RUNNABLE" -> "TIMED_WAITING" 상태가 된다.
  • 이때 반복문을 사용해서 1초마다 큐에 빈자리가 있는지 반복해서 확인한다.
    • 빈 자리가 있다면 큐에 데이터를 입력하고 완료된다.
    • 빈자리가 없다면 sleep() 으로 잠시 대기한 다음 반복문을 계속해서 수행한다. 1초마다 한 번씩 체크하 기 때문에 "큐가 가득 참, 생산자 대기"라는 메시지가 계속 출력될 것이다.

여기서 핵심은 p3 스레드가 락을 가지고 있는 상태에서, 큐에 자리가 나올 까지 대기한다는 점이다.

23:44:04.438 [     main] 현재 상태 출력, 큐 데이터: [data1, data2]
23:44:04.439 [     main] producer1: TERMINATED
23:44:04.440 [     main] producer2: TERMINATED
23:44:04.441 [     main] producer3: TIMED_WAITING

소비자 스레드 실행 시작

실행 결과 분석 - 시간의 흐름6

23:44:04.442 [     main] 소비자 시작
23:44:04.445 [consumer1] [소비 시도]     ? <- [data1, data2]

실행 결과 분석 - 시간의 흐름7

무한 대기 문제

  • c1 이 임계 영역에 들어가기 위해 락을 획득하려 한다.
  • 그런데 락이 없다! 왜냐하면 p3가 락을 가지고 임계 영역에 이미 들어가 있기 때문이다. p3가 락을 반납하기 전까지는 c1 절대로 임계 영역(여기서는 synchronized ) 들어갈 없다!
  • 여기서 심각한 무한 대기 문제가 발생한다.
    • p3 가 락을 반납하려면 소비자 스레드인 c1 이 먼저 작동해서 큐의 데이터를 가져가야 한다.
    • 소비자 스레드인 c1이 락을 획득하려면 생산자 스레드인 p3가 먼저 락을 반납해야 한다.
  • p3는 락을 반납하지 않고, c1은 큐의 데이터를 가져갈 수 없다.
  • 지금 상태면 p3는 절대로 락을 반납할 수 없다. 왜냐하면 락을 반납하려면 c1이 먼저 큐의 데이터를 소비해야 한다. 그래야 p3가 큐에  data3을 저장하고 임계 영역을 빠져나가며 락을 반납할 수 있다. 그런데 p3가 락을 가지고 임계 영역 안에 있기 때문에, 임계 영역 밖의 c1은 락을 획득할 수 없으므로, 큐에 접근하지 못하고 무한 대기한다.
  • 결과적으로 소비자 스레드인 c1 p3 락을 반납할 까지 "BLOCKED" 상태로 대기한다

실행 결과 분석 - 시간의 흐름8

23:44:04.547 [consumer2] [소비 시도]     ? <- [data1, data2]
  • c2 도 마찬가지로 락을 얻을 수 없으므로 "BLOCKED" 상태로 대기한다.

실행 결과 분석 - 시간의 흐름9

23:44:04.652 [consumer3] [소비 시도]     ? <- [data1, data2]
  • c3  마찬가지로 락을 얻을  없으므로 "BLOCKED" 상태로 대기한다.
23:44:04.757 [     main] 현재 상태 출력, 큐 데이터: [data1, data2]
23:44:04.758 [     main] producer1: TERMINATED
23:44:04.758 [     main] producer2: TERMINATED
23:44:04.759 [     main] producer3: TIMED_WAITING
23:44:04.759 [     main] consumer1: BLOCKED
23:44:04.759 [     main] consumer2: BLOCKED
23:44:04.760 [     main] consumer3: BLOCKED
23:44:04.761 [     main] == [생산자 먼저 실행] 종료, BoundedQueueV2 == 
23:44:05.337 [producer3] [put] 큐가 가득 참, 생산자 대기
23:44:06.343 [producer3] [put] 큐가 가득 참, 생산자 대기
23:44:07.354 [producer3] [put] 큐가 가득 참, 생산자 대기

Process finished with exit code 130 (interrupted by signal 2:SIGINT)
  • 결과적으로 c1 , c2 , c3는 모두 락을 획득하기 위해 "BLOCKED" 상태로 대기한다.
  • p3는 1초마다 한 번씩 깨어나서 큐의 상태를 확인한다. 그런데 본인이 락을 가지고 있기 때문에 다른 스레드가 임계 영역 안에 들어오는 것이 불가능하다. 따라서 다른 스레드는 임계 영역 안에 있는 큐에 접근조차 할 수 없다.
  • 결국 p3는 절대로 비워지지 않는 큐를 계속 확인하게 된다. 그리고 "[put] 큐가 가득 참, 생산자 대기"를 1초마다 계속 출력한다.

소비자 스레드 -> 생산자 스레드 실행 결과 분석

실행 결과 분석 - 초기 상태

소비자 스레드 시작

실행 결과 분석 - 시간의 흐름1

23:56:27.477 [     main] == [소비자 먼저 실행] 시작, BoundedQueueV2 == 

23:56:27.481 [     main] 소비자 시작
23:56:27.490 [consumer1] [소비 시도]     ? <- []
23:56:27.491 [consumer1] [take] 큐에 데이터가 없음, 소비자 대기
  • 소비자 스레드인 c1 은 임계영역에 들어가기 위해 락을 획득한다.
  • c1 은 큐의 데이터를 획득하려 하지만, 데이터가 없다.
  • c1 은 sleep(1000)을 사용해서 잠시 대기한다. 이때 "RUNNABLE" -> "TIMED_WAITING" 상태가 된다.
  • 이때 반복문을 사용해서 1초마다 큐에 데이터가 있는지 반복해서 확인한다.
    • 데이터가 있다면 큐의 데이터를 가져오고 완료된다.
    • 데이터가 없다면 반복문을 계속해서 수행한다. 1초마다 "큐에 데이터가 없음, 소비자 대기"라는 메시지가 출력될 것이다.

실행 결과 분석 - 시간의 흐름2

23:56:27.596 [consumer2] [소비 시도]     ? <- []
23:56:27.700 [consumer3] [소비 시도]     ? <- []

23:56:27.806 [     main] 현재 상태 출력, 큐 데이터: []
23:56:27.823 [     main] consumer1: TIMED_WAITING
23:56:27.824 [     main] consumer2: BLOCKED
23:56:27.824 [     main] consumer3: BLOCKED

무한 대기 문제

  • c2 , c3 가 임계 영역에 들어가기 위해 락을 획득하려 한다.
  • 그런데 락이 없다! 왜냐하면 c1이 락을 가지고 임계 영역에 들어가 있기 때문이다. c1이 락을 반납하기 전까지는 c2 , c3는 절대로 임계 영역(여기서는 synchronized )은 들어갈 수 없다!
  • 여기서 심각한 무한 대기 문제가 발생한다.
  • c1 락을 반납하지 않기 때문에 c2 , c3는는 "BLOCKED" 상태가 된다.

생산자 스레드 실행 시작

실행 결과 분석 - 시간의 흐름3

23:56:27.824 [     main] 생산자 시작
23:56:27.827 [producer1] [생산 시도] data1 -> []
23:56:27.927 [producer2] [생산 시도] data2 -> []
23:56:28.032 [producer3] [생산 시도] data3 -> []

무한 대기 문제

  • p1 , p2 , p3가 임계 영역에 들어가기 위해 락을 획득하려 한다.
  • 그런데 락이 없다! 왜냐하면 c1이 락을 가지고 임계 영역에 들어가 있기 때문이다. c1이 락을 반납하기 전까지는 p1 , p2 , p3는 절대로 임계 영역(여기서는 synchronized )은 들어갈 수 없다!
  • 여기서 심각한 무한 대기 문제가 발생한다.
    • c1 이 락을 반납하려면, 생산자 스레드인 p1 , p2 , p3 가 먼저 작동해서 큐의 데이터를 추가해야 한다.
    • 생산자 스레드( p1 , p2 , p3 )가 락을 획득하려면 소비자 스레드인 c1 이 먼저 락을 반납해야 한다.
  • c1은 락을 반납하지 않고, p1은 큐에 데이터를 추가할 수 없다.(물론 p2 , p3도 포함이다.)
  • 지금 상태면, c1은 절대로 락을 반납할 수 없다. 왜냐하면 락을 반납하려면 p1이 먼저 큐의 데이터를 추가해야 한다. 그래야 c1이 큐에서 데이터를 획득하고 임계 영역을 빠져나가며 락을 반납할 수 있다. 그런데 c1이 락을 가지고 임계 영역 안에 있기 때문에, 임계 영역 밖의 p1은 락을 획득할 수 없으므로, 큐에 접근하지 못하고 무한 대기한다.
  • 결과적으로 생산자 스레드인 p1 c1 락을 반납할 까지 "BLOCKED" 상태로 대기한다.
23:56:28.138 [     main] 현재 상태 출력, 큐 데이터: []
23:56:28.139 [     main] consumer1: TIMED_WAITING
23:56:28.140 [     main] consumer2: BLOCKED
23:56:28.140 [     main] consumer3: BLOCKED
23:56:28.140 [     main] producer1: BLOCKED
23:56:28.140 [     main] producer2: BLOCKED
23:56:28.141 [     main] producer3: BLOCKED
23:56:28.142 [     main] == [소비자 먼저 실행] 종료, BoundedQueueV2 == 
23:56:28.496 [consumer1] [take] 큐에 데이터가 없음, 소비자 대기
23:56:29.502 [consumer1] [take] 큐에 데이터가 없음, 소비자 대기
23:56:30.509 [consumer1] [take] 큐에 데이터가 없음, 소비자 대기
  • 결과적으로 c1을 제외한 모든 스레드가 락을 획득하기 위해 "BLOCKED" 상태로 대기한다.
  • c1은 1초마다 한 번씩 깨어나서 큐의 상태를 확인한다. 그런데 본인이 락을 가지고 있기 때문에 다른 스레드는 임계 영역에 들어오는 것이 불가능하고, 큐에 접근조차 할 수 없다.
  • 따라서 "[take] 큐에 데이터가 없음, 소비자 대기"를 1초마다 계속 출력한다.

 

정리
버퍼가 비었을 때 소비하거나, 버퍼가 가득 찾을 때 생산하는 문제를 해결하기 위해, 단순히 스레드가 잠깐 기다리면 될 것이라 생각했는데, 문제가 더 심각해졌다. 생각해 보면 결국 임계 영역 안에서 락을 가지고 대기하는 것이 문제이다. 이것은 마치 열쇠를 가진 사람이 안에서 문을 잠가버린 것과 같다. 그래서 다른 스레드가 임계 영역 안에 접근조차 할 수 없는 것이다.

 

여기서 잘 생각해 보면, 락을 가지고 임계 영역 안에 있는 스레드가 sleep()을 호출해서 잠시 대기할 때는 아무 일도 하지 않는다.
그렇다면 이렇게 아무일도 하지 않고 대기하는 동안 잠시 다른 스레드에게 락을 양보하면 어떨까? 그러면 다른 스레드가 버퍼에 값을 채우거나 버퍼의 값을 가져갈 수 있을 것이다.
그러면 락을 가진 스레드도 버퍼에서 값을 획득하거나 값을 채우고 락을 반납할 수 있을 것이다.


예를 들어 락을 가진 소비자 스레드가 임계 영역 안에서 버퍼의 값을 획득하기를 기다린다고 가정하자. 버퍼에 값이 없으면 값이 채워질 때까지 소비자 스레드는 아무 일도 하지 않고 대기해야 한다.
어차피 아무일도 하지 않으므로, 이때 잠 시 락을 다른 스레드에게 빌려주는 것이다. 락을 획득한 생산자 스레드는 이때 버퍼에 값을 채우고 락을 반납한다. 버퍼에 값이 차면 대기하던 소비자 스레드가 다시 락을 획득한 다음에 버퍼의 값을 가져가고 락을 반납하는 것이다.

"락을 가지고 대기하는 스레드가 대기하는 동안 다른 스레드에게 락을 양보할 수 있다면, 이 문제를 쉽게 풀 수 있다."

 

728x90