[Java] Thread Pool 개념과 동작원리

H43RO·2021년 10월 19일
9

CS 뿌셔먹기

목록 보기
13/17

Thread Pool 이 등장하게 된 이유

우리는 프로그램을 개발할 때 다양한 이유로 쓰레드를 생성하곤 한다. 그것은 비동기 처리의 일환일 수도 있고, 연산 효율을 높이기 위함일 수도 있다. 그러나 쓰레드를 계속하여 생성하고 회수하는 것은, 시스템적으로 오버헤드가 상당히 큰 작업이다.

쓰레드를 한 번 생성할 때마다 OS 가 해당 쓰레드를 위한 메모리 영역 (스택 등) 을 확보해주고, 쓰레드가 더이상 필요 없을 땐 다시 이 메모리 영역을 회수하는 작업이 일어난다. 이는 상당히 비용이 큰 작업이기 때문에, 쓰레드를 계속하여 생성하고 수거했다간 프로그램의 퍼포먼스에 분명히 영향을 끼치게 되어있다.

그렇다고 '음.. 걍 쓰레드 많이 만들지 마세요 ㅋㅋ' 할 순 없는 노릇이다. 그래서 등장하게 된 아이디어는 '여러 쓰레드를 미리 만들어두고 작업이 들어올 때마다 쓰레드들에게 작업을 적절히 분배해주자'는 것이다.

핵심 아이디어

작업 들어올 때마다 쓰레드 생성 (X)

작업 들어올 때마다 미리 만들어져 있는 쓰레드들 중 하나에 작업 할당 (O)

이렇게 되면 쓰레드 생성 및 수거에 대한 오버헤드를 대폭 줄일 수 있기 때문에, 꽤나 효율적인 대안이다. 따라서 자바에서는 이를 적극 지원하기 위해 ExecutorService 라는 인터페이스와 Executor 클래스를 제공한다. 이들을 사용하여 쓰레드 풀을 구현할 수 있다.


동작원리

아래 그림으로 대략적인 동작을 유추해볼수 있다. Thread Pool많은 쓰레드를 미리 생성해두고, 새로운 작업이 들어올 때마다 Task Queue 에 이를 Enqueue 한다. 그리고 작업을 큐에서 하나씩 꺼내어 적절한 쓰레드로 할당하게 된다. 그리고 만약 작업이 끝나면, 이를 콜백 형태로 작업을 요청한 주체에게 결과를 알려준다.

쓰레드에 작업을 할당하는 동작을 조금 더 명료하게 나타낸 그림도 첨부한다. 새로운 작업을 처리할 때 적절한 쓰레드를 선택하여 해당 작업을 할당한다는 의미는, 작업을 수행하지 않고 있는 쓰레드에 작업을 할당한다는 뜻으로 받아들여도 될 것같다.


장점

퍼포먼스 저하 방지

앞서말했던 것처럼, 쓰레드를 계속하여 생성하고 수거하는 것은 비용이 상당히 큰 작업이다. 따라서 쓰레드 풀을 사용하게 되면 비용이 대폭 줄어들어 성능 저하를 방지할 수 있다.

다수의 요청을 효율적으로 처리

이곳 저곳에서 작업 요청이 들어올 수 있다. 서비스 측면에서 빗대어 본다면, 여러 사용자들이 한 번에 작업 요청을 보낼 때 쓰레드풀을 사용한다면 빠르고 효율적으로 동시 작업을 수행할 수 있다.

그런데 결국 마냥 좋은 것도 아니다. 쓰레드 풀이 가지는 단점에 대해 살펴보자면 아래와 같다.


단점

자칫 메모리 낭비로 이어질 수 있음

계속 말하지만 쓰레드 풀은 일정 가량 쓰레드를 미리 만들어두고 이들을 적절히 활용하는 솔루션이다. 결국 얼만큼 쓰레드를 만들어 둘지에 대해 결정해야 하는데, 이 과정에서 '에잇 언젠간 다 쓰겠지 머' 하고 너무 많은 쓰레드를 생성해두게 되면 메모리만 차지하고 아무것도 하지 않는 쓰레드가 존재할 가능성이 높아진다.

놀고먹는 쓰레드가 발생할 수 있음

위 단점과는 조금 다르다. 병렬적으로 A, B, C 쓰레드가 작업을 처리하는 과정에서, 각각 분배된 작업의 소요시간이 서로 다른 경우, A 는 아직 허덕이고 있는데 나머지는 '음 쟤 고생하네 ㅋㅋ'하면서 유휴 시간이 발생하는 상황이 발생할 수 있다. 유휴한 쓰레드들이 A 의 작업을 조금만 도와준다면 좋을텐데.

자바에선 이러한 상황을 방지하기 위해 forkJoinPool 이라는 것을 지원한다.


종류

Single Thread Executor

  • 단일 쓰레드를 생성
  • 실패 시 새로 쓰레드를 생성하진 않음
ExecutorService executorService = Executors.newSingleThreadExecutor();

Fixed Thread Executor

  • 고정된 개수의 쓰레드를 생성하고, 모든 쓰레드가 작업 중이라면 Task Queue 에 작업 적재
  • 실패 시 새로운 쓰레드를 생성하여 대체함
ExecutorService executorService = Executors.newFixedThreadPool(int nThreads);

Cached Thread Pool

  • 필요에 따라 새로운 쓰레드를 생성하며, 이전에 생성했던 쓰레드가 존재한다면 이를 재사용함
  • 비동기 작업에 사용하기 좋은 녀석
  • 기본적으로 60초 동안 쓰레드가 유지됨
ExecutorService executorService = Executors.newCachedThreadPool();

Scheduler Thread Pool

  • 지정된 Delay 후에 실행하거나 주기적으로 실행하도록 작업 예약
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(int corePoolSize);

Work Stealing Thread Pool

  • 자바 8에서 새로 생긴 녀석

  • 쓰레드를 동적으로 늘리고 줄일 수 있음

  • 요청된 병렬 동작을 지원할만큼 충분한 쓰레드를 유지하고, 쓰레드마다 독립적인 큐를 사용

  • 따라서 작업 큐가 비어있는 쓰레드가 존재한다면, 다른 쓰레드의 작업 큐에서 작업을 훔쳐(?)올 수 있음

    놀고 먹는 쓰레드에게 다른 쓰레드의 일을 좀 도와줄 수 있도록 함

  • 위 특성때문에, 작업이 실행되는 순서를 보장하진 않음. (큐잉 순서와 무관하게 동작)

ExecutorService executorService = Executors.newWorkStealingPool(int parallelism);

쓰임새

쓰레드 풀은 알게 모르게 상당히 유용하게 사용되고 있다. 우리에게 가장 친숙한(?) 녀석으로는 RxJava 가 있다. Observable 데이터 스트림의 메소드인 subscribeOn() 을 통해 해당 데이터 스트림이 어떤 쓰레드에서 데이터를 발행할지 지정해줄수 있다. 이 때 매번 쓰레드를 생성하여 데이터 스트림을 할당하는 것이 아닌, 쓰레드 풀에서 적절한 쓰레드를 선택하여 작업을 할당해주게 된다.


참고자료

https://limkydev.tistory.com/55
https://en.wikipedia.org/wiki/Thread_pool

profile
어려울수록 기본에 미치고 열광하라

1개의 댓글

comment-user-thumbnail
2025년 4월 29일

That's a great overview of Java Thread Pools! Understanding how they manage threads efficiently is key for building scalable applications. It reminds me of optimizing resource allocation in games, like ensuring smooth gameplay even with complex calculations, a bit like navigating challenges in Slope Unblocked. Keeping the game running smoothly. https://slopeunblockedd.org

답글 달기