JAVA/Effective Java

[아이템 84] 프로그램의 동작을 쓰레드 스케줄러에 기대지 말라

꾸준함. 2024. 4. 22. 02:28

1. 서론

  • 여러 쓰레드가 실행 중일 경우 OS의 쓰레드 스케줄러가 어떤 쓰레드를 얼마나 오래 실행할지 결정
  • OS마다 구체적인 스케줄링 정책은 다를 수 있기 때문에 특정 정책에 의존해서는 안됨
  • 정확성이나 성능이 쓰레드 스케줄러에 종속적인 프로그램이라면 타 플랫폼에 이식하기 어려움

 

2. 좋은 프로그램을 작성하기 위한 원칙

다음 원칙을 지키면 쓰레드 스케줄링 정책이 변경되어도 크게 영향받지 않습니다.

  • 프로세서 수보다 실행 가능한 쓰레드의 평균 수가 지나치게 많아지지 않도록 설정
  • 실행 준비가 된 쓰레드들은 맡은 작업을 완료할 때까지 계속 실행되어야 함

 

3. 실행 가능한 쓰레드 수를 적게 유지하기 위한 원칙

  • 각 쓰레드가 작업을 완료한 뒤 다음 작업이 생길 때까지 대기하도록 하는 것이 중요
    • 이는 ThreadPool이라고도 알려진 실행자 프레임워크에서 자주 사용되는 개념
    • 그러나, 작업이 너무 짧을 경우에는 업을 분배하는 과정이 작업 자체보다 더 많은 시간과 자원을 요구할 수 있어 쓰레드의 생성 및 관리에 따른 부담이 오히려 성능 저하를 일으킬 수 있음
    • 따라서, 작업이 너무 짧을 경우에는 작업을 짧게 유지하는 것이 아니라, 적절한 크기의 쓰레드 풀을 유지하는 것이 중요

 

3.1 예시

  • 쓰레드 풀은 특정 작업을 처리하기 위해 미리 생성된 쓰레드의 집합
  • 작업이 들어오면 이 쓰레드 풀에서 사용 가능한 쓰레드가 해당 작업을 처리
  • 작업이 완료되면 해당 쓰레드는 대기 상태로 회귀
  • 이렇게 함으로써 쓰레드의 생성 및 소멸에 따른 오버헤드를 최소화하고, 자원을 효율적으로 활용 가능

 

4. 쓰레드와 바쁜 대기 상태

  • 임계 영역에서 작업 중인 쓰레드 B가 작업 완료할 때까지 대기하는 쓰레드 A가 있을 때 쓰레드 A가 임계 영역에 들어갈 수 있는지 계속해서 검사하는 상태를 바쁜 대기(busy waiting) 상태라고 함
  • 쓰레드는 절대 바쁜 대기 상태가 되어서는 안 됨
    • 바쁜 대기 상태는 쓰레드 스케줄러의 변덕에 취약하며, 프로세서에 큰 부담을 주어 다른 유용한 작업이 실행될 기회를 박탈할 수 있음
    • 바쁜 대기 상태에서는 쓰레드 A가 임계 영역에 진입 가능한지를 계속해서 확인하는데, 이는 쓰레드 스케줄러가 어떤 쓰레드를 언제 실행시킬지에 따라 결과가 달라지므로 대기 상태에서는 쓰레드의 실행 순서가 예측하기 어려워짐
    • 바쁜 대기 상태에서는 쓰레드가 계속해서 프로세서를 점유하고 있어야 하며 이는 다른 유용한 작업을 실행하기 위해 프로세서를 활용할 수 없다는 것을 의미
    • 대기 상태에 있는 쓰레드가 임계 영역에 진입 가능한지 여부를 확인하는 데에는 바쁜 대기 상태를 사용하는 것보다는 대기열, 세마포어, 뮤텍스 등과 같은 동기화 기법을 활용하는 것이 바람직

 

4.1 바쁜 대기 상태의 CountDownLatch 예시 코드

 

 

 

코드 부연 설명

  • 자바의 CountDownLatch보다 약 10배 정도 느린 코드
    • 대기 및 카운트 다운 작업이 모두 synchronized 블록 내에서 이루어지기 때문
    • await() 메서드는 계속해서 임계 영역을 반복하여 확인하고 있으며 이 과정에서 대기하고 있는 쓰레드가 불필요하게 프로세서를 점유하며, 바쁜 대기 상태에 빠질 가능성 존재
    • countDown() 메서드도 동기화 블록 내에서 실행되기 때문에 여러 쓰레드가 동시에 이 메서드를 호출할 경우 대기 중인 쓰레드들이 성능적으로 이점을 얻지 못하고 경합 상태에 빠질 수 있음

 

  • 반면, CountDownLatch는 내부적으로 비교적 효율적으로 구현되어 있음
    • 대기 및 카운트 다운 작업이 동기화 블록 내에서 분리되어 있고, await() 메서드는 조건 변수를 사용하여 쓰레드를 대기시키기 때문에 바쁜 대기 상태에 빠지지 않음
    • 또한, 카운트 다운 작업은 원자적으로 이루어지므로 경합 상태가 발생하지 않음

 

5. Thread.yield를 써서 문제를 고쳐보려는 유혹을 떨쳐내자

  • Thread.yield()를 호출하면 현재 실행 중인 쓰레드가 다른 쓰레드에게 CPU 사용을 양보
    • 이는 실행 중인   레드의 상태를 변경하므로 프로그램의 동작이 불안정해질 수 있으며 특히, 다른  레드들이 충분한 CPU 시간을 얻지 못하고 있는 상황에서 이러한 양보는 예기치 않은 동작을 유발

 

  • Thread.yield() 레드 스케줄러에게 현재 스레드의 CPU 사용을 양보한다는 신호를 보내는 것이므로, 불필요한 양보는 성능 저하를 초래할 수 있으며 특히, 시스템에 많은 쓰레드가 활동 중이고 CPU 자원이 제한적인 경우 더 심해질 가능성이 있음
  • Thread.yield()를 호출하여 쓰레드 스케줄러의 동작에 의존하는 것은 좋지 않은 프로그래밍 관행
    • 쓰레드 스케줄러의 동작은 플랫폼에 따라 다를 수 있으며, 예측하기 어려우며 이는 프로그램의 이식성 및 안정성을 해칠 수 있음

 

  • 게다가 Thread.yield()를 테스트할 수단이 없기 때문에 추천하지 않음

 

6. 쓰레드 우선순위를 조정하는 것도 위험이 따름

  • 쓰레드 우선순위는 자바에서 이식성이 가장 나쁜 특성에 속함
    • 자바의 다른 특성들과 달리 플랫폼 간에 일관된 동작을 보장하기 어렵기 때문
    • 자바에서는 쓰레드 우선순위를 설정하여 특정 쓰레드가 CPU 자원을 얻을 확률을 높이거나 낮출 수 있지만 이 우선순위는 JVM 및 운영체제의 쓰레드 스케줄러에 의해 해석되고 처리되기 때문에 이식성이 낮음
    • OS마다 다른 쓰레드 스케줄러 정책을 가져가기 때문에 예측하기 쉽지 않음

 

  • 쓰레드 몇 개의 우선순위를 조율해서 애플리케이션의 반응 속도를 높이는 것이 가능한 케이스도 있겠지만 이러한 상황은 드물고 이식성이 떨어지기 때문에 추천하지 않음

 

정리

  • OS마다 쓰레드 스케줄러 정책을 달리 가져가므로 프로그램의 동작을 쓰레드 스케줄러에 기대지 말자
    • 이는 견고성과 이식성을 모두 해치는 행위

 

  • 같은 이유로 Thread.yield()와 쓰레드 우선순위에 의존하는 코드를 작성하지 말자
    • 해당 기능들은 단순히 쓰레드 스케줄러에서 제공하는 힌트일 뿐

 

참고하면 좋은 카테고리

 

https://jaimemin.tistory.com/category/JAVA/RxJava

 

'JAVA/RxJava' 카테고리의 글 목록

메일: jaimemin@naver.com

jaimemin.tistory.com

 

참고

이펙티브 자바

 

반응형