CS

[OS] 스레드 풀(Thread Pool)

chocoji 2024. 4. 26. 00:56

스레드 풀이란?

스레드 + 풀

  • `스레드(Thread)`: 프로세스에서 실행되는 흐름의 단위
  • `풀(Pool)`: 필요할 때마다 객체를 할당하고 해제하는 대신, 사용 가능한 초기화된 객체의 집합
  • `스레드 풀(Thread Pool)`: 미리 일정 개수의 스레드를 생성해 두고 작업이 발생하면 사용 가능한 스레드를 할당하여 작업을 처리하는 방식으로, 처리가 완료되면 스레드는 제거되지 않고 다시 스레드 풀로 돌아가 스레드의 반복적인 생성과 소멸을 피하고 시스템 자원을 효율적으로 활용할 수 있다.

 

스레드 풀의 필요성

데이터베이스, 웹 서버 등의 서버 프로그램은 여러 클라이언트의 요청을 반복적으로 실행한다. 이를 처리하기 위해 다음과 같이 서버에 들어오는 요청마다 스레드를 새로 만들어서 처리하고 처리가 끝난 스레드를 버리는 식으로 동작한다면 어떻게 될까?

  • 매 요청마다 스레드 생성에 소요되는 시간이 추가적으로 소요되므로 요청 처리가 더 오래 걸린다는 단점이 있으며,
  • 서버의 처리 속도보다 더 빠르게 요청이 들어온다면 다음과 같은 문제가 발생한다.
    • 스레드 계속 생성 ➡️ 컨텍스트 스위칭 더 자주 발생 ➡️ CPU 오버헤드 증가
      ➡️ 어느 순간 서버 전체가 응답 불가능한 상태에 빠짐 ➡️ 메모리가 점점 고갈됨

즉, 스레드 생성과 소멸 작업으로 인한 리소스 소모와 성능 저하 문제가 발생한다.

이때 스레드 풀을 사용해서 스레드를 재활용하면 시스템 자원을 효율적으로 활용하고 작업을 더 빠르게 처리할 수 있다.

  1. `Thread Pool`에 미리 일정 개수의 스레드를 생성한다.
  2. `작업 큐(Queue)`에 들어오는 Task를 하나씩 꺼내서 사용 가능한 `Thread` 중 하나를 할당하여 처리한다.
  3. 작업이 완료되면 해당 스레드는 다시 풀에 반환된다. 작업 결과는 클라이언트에게 반환되거나 필요한 경우 다음 작업에 전달된다.

 

장단점

  • 장점
    • 성능 향상: 스레드의 반복적인 생성과 소멸 작업을 수행하지 않고 미리 생성된 스레드를 재활용하므로 시스템 성능 향상
    • 응답 시간 개선: 미리 생성된 스레드를 활용하므로 요청마다 스레드를 생성하는 것보다 더 빠르게 작업을 처리할 수 있음
    • 스레드 개수 제한 가능: 스레드 개수를 제한함으로써 동시에 실행되는 스레드의 수를 제한 가능 ➡️ 작업처리 요청이 갑자기 폭증해도 작업 큐의 작업량만 증가할 뿐 서버 성능은 완만히 저하된다.
  • 단점
    • 메모리 낭비: 미리 생성된 스레드를 관리하기 위해 메모리를 소비하는데, 너무 큰 스레드 풀은 메모리 낭비를 유발하기도 한다. ➡️ 스레드 풀 크기를 적절하게 설정해야 한다.

 

Java에서 스레드 풀 사용하기

스레드 풀 생성

`java.util.concurrent` 패키지에서 `Executors` 클래스를 제공한다.
  • `Executors` 클래스의 정적 메소드를 사용하여 `ExecutorService`라는 인터페이스의 구현체를 생성할 수 있다. ➡️ 이 구현체가 바로 스레드 풀!
  • 용어 정리
    • `초기 스레드 수`: 스레드 풀이 생성될 당시의 스레드 개수
    • `코어 스레드 수`: 스레드 풀 내의 스레드가 제거되어도 남아있을 최소 스레드 개수
    • `최대 스레드 수`: 스레드 풀 내 스레드의 최대 개수

 

FixedThreadPool - 고정 크기

ExecutorService executorService = Executors.newFixedThreadPool(200); //스레드 수: 200개
  • 초기 스레드 수, 코어 스레드 수: 0
  • 최대 스레드 수: `Integer.MAX_VALUE`
  • 작업이 들어올 때마다 스레드를 생성하여, 생성된 스레드가 60초 동안 아무 것도 하지 않으면 해당 스레드를 제거한다.

 

CachedThreadPool - 가변 크기

ExecutorService executorService = Executors.newCachedThreadPool();
  • 초기 스레드 수: 0
  • 코어 스레드 수, 최대 스레드 수: 스레드 풀이 생성될 때 지정할 수 있다.
  • `존재하는 스레드 개수 < 작업량`일 때 스레드를 생성하며, 생성된 스레드는 유휴 상태가 오래 되었다고 해도 제거하지 않는다.
    • 스레드 수가 폭발적으로 증가할 수 있다는 단점이 있다.

 

ThreadPoolExecutor

ExecutorService myThreadPool = new ThreadPoolExecutor(
        3, // 코어 스레드 수
        200, // 최대 스레드 수
        120, // 최대 유휴 시간
        TimeUnit.SECONDS, // 최대 유휴 시간 단위
        new SynchronousQueue<>() // 작업 큐
);
  • `ThreadPoolExecutor`를 사용해서 원하는 대로 커스텀할 수도 있다.

 

스레드 풀 종료

`ExecutorService`는 스레드 풀 종료를 위해 3가지 메소드를 제공한다.
threadPool.shutdown();
List<Runnable> unprocessedTasks = threadPool.shutdownNow();
boolean complete = threadPool.awaitTermination(60, TimeUnit.SECONDS);
  • `shutdown()`: 현재 스레드가 처리 중인 작업과 작업 큐에 대기하고 있는 작업을 모두 끝 마친 뒤 스레드 풀을 종료한다.
  • `shutdownNow()`: 현재 작업 중인 강제로 종료시킨 뒤, 작업 큐 대기열에 남아있는 작업을 `List<Runnable>`로 반환한다.
  • `awaitTermination()`: `shutdown()`을 먼저 호출하고, 전달된 시간 내로 모든 작업이 완료되면 `true`를, 완료하지 못하면 처리 중인 스레드를 강제 종료하고 `false`를 반환한다.

 

Ref

https://www.youtube.com/watch?v=B4Of4UgLfWc&t=781s

https://en.wikipedia.org/wiki/Object_pool_pattern

https://en.wikipedia.org/wiki/Thread_pool

https://www.geeksforgeeks.org/thread-pools-java/?ref=gcse

https://velog.io/@dodozee/OSJava-%EC%8A%A4%EB%A0%88%EB%93%9C-%ED%92%80%EC%9D%B4%EB%9E%80Thread-pool

https://codechacha.com/ko/java-executors/

https://hudi.blog/java-thread-pool/

https://simyeju.tistory.com/119