티스토리 뷰

언어/Java

[Java]쓰레드 풀(Thread Pool)에 대하여

꼬마우뇽이(원종운) 2021. 12. 31. 21:53

1편 - 쓰레드(Thread)에 대하여

2편 - 쓰레드 상태(Thread State)와 메서드에 대하여 

3편 - 쓰레드 동기화(Thread Synchronization)에 대하여

4편 - 쓰레드 풀(Thread Pool)에 대하여 - 현재 글

5편 - ThreadExecutor에 대하여

6편 - ForkJoinPool에 대하여


쓰레드 풀(Thread Pool)이 무엇일까?

쓰레드 풀은 주어진 작업 처리를 위하여 미리 쓰레드를 정해진 개수만큼 생성하여 보관함(Pool)에 보관하여 사용하는 디자인 패턴입니다.

 

쓰레드 풀의 목적은 무엇일까?

프로세스의 생성보다 비용이 작은 것은 사실이나, 쓰레드를 생성하는 비용도 무시할 수 없습니다. 매번 쓰레드를 새로 생성하여 사용한 후 수거하는 건 많은 비용을 요구하기 때문에 성능 저하가 발생할 수 있습니다. 그러한 부분을 해결하기 위하여 미리 쓰레드를 생성하여둔 보관함에서 사용 가능한 쓰레드를 꺼내어 사용한 후 반납하게 될 경우 쓰레드를 재사용할 수 있기 때문에 성능 저하 이슈를 해결할 수 있습니다.

 

쓰레드를 많이 만들수록 좋은 거 아닌가?

JVM은 쓰레드 생성 개수에 별도의 제약을 두지 않기 때문에 계속적으로 쓰레드를 만들 수는 있으나 그렇게 할 경우 메모리 부족이 발생할 수 있고 쓰레드 간의 빈번한 컨텍스트 스위칭(Context-Switching)등의 문제로 성능이 오히려 저하가 될 수 있습니다.

 

쓰레드 풀의 기본적인 작동방식

작동 방식

1. 클라이언트가 작업(Task)을 요청(Submit)하면 Thread Pool 내부에 존재하는 작업 큐(Work Queue, Task Queue)에 해당 작업을 넣습니다.

2. 쓰레드 풀은 작업 큐에 존재하는 작업을 순차적으로 꺼내어 유휴(Idle) 상태인 쓰레드에게 작업을 실행하도록 합니다.

3. 클라이언트는 해당 작업이 완료(Complete)되면 요청한 작업에 대한 결과(result)를 받는 방식으로 작동합니다.

 

JDK에서 기본으로 제공하는 쓰레드 풀에 대해서 알아보자

JDK 1.5 이전에는 사용자들이 직접 쓰레드 풀을 만들어 사용하여야 했습니다. 이 작업을 어렵고 까다로웠을 뿐만 아니라 컨텍스트 스위칭(Context-Switching)과 같은 여러 가지 문제들이 존재했습니다. JDK 1.5 이후에는 그러한 점을 해결하기 위하여 쓰레드 풀을 쉽게 사용할 수 있도록 제공하고 있습니다.

 

ThreadExecutor - Since JDK 1.5

JDK 1.5 이후로 제공되는 쓰레드 풀 구현체입니다.

 

ThreadExecutor 생성자

public ThreadPoolExecutor(
	int corePoolSize,
	int maximumPoolSize,
	long keepAliveTime,
	TimeUnit unit,
	BlockingQueue<Runnable> workQueue,
	ThreadFactory threadFactory,
	RejectedExecutionHandler handler
);
인수 설명
corePoolSize 유휴 상태일때도 쓰레드 풀에 유지할 쓰레드 수입니다.
maximumPoolSize 쓰레드 풀에 허용할 최대 쓰레드 수입니다.
keepAliveTime 쓰레드 수가 코어의 수보다 많을 경우, 초과 유휴 쓰레드가 종료되기전에 새로운 작업을 기다리는 최대 시간입니다.
unit keepAliveTime 인수의 시간 단위입니다.
workQueue 작업이 실행되기전에 잡혀있는 작업큐입니다. 
threadFactory Executor가 새로운 쓰레드를 생성할 때 사용하는 팩토리입니다.
handler 쓰레드 경계 및 대기열 용량에 도달하여 실행이 차단될 때 사용할 핸들러입니다.

 

ScheduledThreadExecutor - Since JDK 1.5

JDK 1.5 이후로 제공되는 쓰레드 풀 구현체 중 하나이며, 일정 시간마다 주기적으로 반복해야 하는 작업에 적합합니다. ThreadExecutor의 인수를 적절하게 조절하여 구현됩니다.

 

ForkJoinPool - Since JDK 1.7

ForkJoinPool이 다른 쓰레드 풀과 다른 점은 큰 작업을 작은 작업 단위로 나누고(Fork), 그것을 다른 쓰레드에서 병렬로 처리한 후 결과를 취합(Join)하는 방식이며, 분할-정복 메커니즘과 비슷하다는 점입니다.

 

작업을 나눈다(Fork)
결과를 취합한다(Join)

 

ForkJoinPool의 특징 Work-Stealing 알고리즘

또 앞서 살펴본 ThreadExecutor, ScheduledThreadExecutor는 ExecutorService의 구현체들인데, 이러한 구현체들과의 또 다른 차이점은 Work-Stealing 알고리즘이 적용되어있다는 것입니다. 여러 작업을 쓰레드 풀을 이용하여 분배를 한다고 해도 어떻게 분배하냐에 따라 일부 쓰레드는 계속 유휴 상태일 수 있으며, 이는 사용 가능한 쓰레드가 있음에도 불구하고 비효율적일 수 있습니다.

 

하지만 ForkJoinPool의 Work-Stealing 알고리즘은 이러한 부분을 효과적으로 해결하여줍니다. 다른 쓰레드가 할 작업이 많다면 그렇지 않은 유휴 한 쓰레드가 작업을 훔쳐와서 수행하여준다는 것입니다. 

 

Work-Stealing 알고리즘 간단한 처리 과정

 

처리 과정을 간단하게 살펴보면 다음과 같습니다. 

 

첫 째, ForkJoinPool 내부에 존재하는 작업 큐(Inbound Task Queue)에 1차적으로 작업이 적재(submit) 됩니다.

둘째, 쓰레드 풀에 존재하는 각 쓰레드들이 작업 큐에서 작업을 가져와 자신의 쓰레드 내부에 존재하는 작업 덱(Deque)에 적재하여 순차적으로 처리합니다.

셋째, 만약 자신의 쓰레드 내부에 존재하는 작업 덱(Deque)에 더 이상 처리할 작업이 없을 경우 다른 쓰레드의 작업을 훔쳐와서 처리하거나 작업 큐(Inbound Task Queue)에도 작업이 있을 경우 가져와서 처리합니다.

 

여기서 의문인 것은 왜 쓰레드 내부에 존재하는 작업 큐와 같은 역할을 하는 큐가 Deque로 구현이 되어있는가?

 

그것은 큐의 특성상 선입선출이기 때문입니다. Work-Stealing 알고리즘은 처리되지 못하고 대기되어있는 작업을 훔쳐 오기 때문에 일반적인 큐라면 대기하고 있는 작업을 가져오는 것이 아니라 가장 앞에 있는 작업을 가져올 수밖에 없기 때문입니다. 

 

(훔칠 작업이 있는지 확인을 하게 될 때마다 Cache-Miss처럼 실패하는 경우가 있을 수 있으며 이러한 경우는 별도의 처리를 해준다고 합니다. 차후에 적어보도록 하겠습니다.)

 

ForkJoinPool 생성자

public ForkJoinPool(
	int parallelism,
	ForkJoinWorkerThreadFactory factory,
	UncaughtExceptionHandler handler,
	boolean asyncMode
);
인수 설명
parallelism 병렬화 수준이며, 기본값으로 코어 수(Runtime.getRuntime.().availableProcessors())를 사용합니다.
factory 새 쓰레드를 생성할 때 사용하는 팩토리입니다.
handle 작업을 처리하는 중 발생한 복구할 수 없는 오류로 인해 종료되는 내부 쓰레드를 처리할 때 사용하는 핸들러입니다.
asyncMode true로 설정할 경우 join 되지 않는 fork된 작업들에 대해 로컬 FIFO 스케쥴링 모드를 설정합니다.

마무리

다음 4편과 5편에서는 각 쓰레드 풀을 보다 쉽게 생성하는 방법과 실제로 작업(Task)을 전달(submit)하여 수행(execute)하는 방법에 대해서 알아보도록 하겠습니다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함