jaesungLeee 2021. 12. 13. 15:07

 

1. Thread Pool

Thread를 생성하면 OS가 메모리 공간을 확보하고 그 메모리를 Thread에 할당한다. 하지만, 수행해야 할 작업의 개수에 따라 병렬 작업 처리가 많아지면 그에 필요한 Thread의 개수가 증가하게 된다. 이 경우, Thread의 생성과 스케줄링으로 인해 OS가 바빠지게 되고 작업을 마치고 수거하는 일은 비용이 크기 때문에 어플리케이션 성능에 큰 영향을 줄 수 있다. 이를 방지하기 위해 Thread Pool을 사용할 수 있다.

 

위에서 설명한 것 처럼 Thread의 생성과 수거는 비용 소모가 많은 작업이다. 미리 Thread를 여러 개 만들어 두고 작업이 들어오는 대로 할당해서 사용하게 하고 작업이 끝나고 난 뒤에도 그대로 뒀다가 나중에 필요할 때 재사용하기 위한 장치가 Thread Pool이다. 

Thread Pool을 사용하면 작업 처리에 사용되는 Thread를 제한된 개수만큼 미리 생성해 놓을 수 있다. 이때, 작업 Queue에 들어오는 작업들을 Thread Pool에 있는 Thread가 하나씩 맡아서 처리하게 되고 작업 처리가 끝난 Thread는 작업 결과를 반환하게 된다. 

 

기본적으로 Thread Pool은 Java에서 java.util.concurrent.Executors와 java.util.concurrent.ExecutorService 패키지를 통해 제공한다. 이를 사용하여 Thread Pool을 생성해 병렬 처리가 가능하다. 

 

Thread Pool을 사용하면 Thread Pool안의 Background Thread는 실행할 다음 Task를 기다리기 위해 계속 존재할 수 있다. 이는 Thread가 Task를 위해 생성되고 제거될 필요가 없다는 의미이다. 결국, 매번 발생되는 Task를 병렬 처리하기 위해 생성/수거하는데 따르는 부담은 성능을 저하시킨다. 또한, 다수의 사용자 요청을 수용하고 빠르게 처리하고 대응할 수 있는 장점이 있다.

하지만, 병럴 처리를 위해 생성해놓은 Thread들이 아무 일도 하지 않고 메모리만 차지하는 경우가 있을 수 있다. 또한, 병렬적으로 Task를 처리하는 과정에서 각각 작업 처리 시간이 다른 경우 Thread 유휴 시간이 발생할 수 있다는 단점이 존재한다. (A Thread는 할 일이 남았는데 B, C가 노는 경우)


2. Thread Pool 생성

Thread Pool 사용을 위해 ExecutorService 인터페이스와 Executors 클래스를 사용한다. 아래에 Thread Pool에 필요한 Thread 3가지 개념을 살펴본다. 

 

- 초기 Thread 수 : ExecutorService 객체가 생성될 때 기본적으로 생성되는 Thread 수

- 코어 Thread 수 : Thread가 증가한 후 사용되지 않은 Thread를 Thread Pool에서 제거할 때 최소한으로 유지해야 하는 Thread 수

최대 Thread 수 : Thread Pool에서 관리하는 최대 Thread의 수

 

Executors.newSingleThreadExecutor( )

Thread가 1개인 Thread Pool을 생성한다. 단일 Thread에서 동작해야 하는 작업을 처리할 때 사용한다.

val executorService = Executors.newSingleThreadExecutor()

Executors.newCachedThreadPool( )

초기, 코어 Thread 수는 0개, 최대 Thread 수는 Integer.MAX_VALUE로 지정한다. 

Thread 개수보다 작업의 개수가 많으면 새로운 Thread를 생성하여 작업을 처리한다. Thread가 60초 동안 작업을 하지 않고 있으면 Thread를 종료하고 Thread Pool에서 제거한다.

val executorService = Executors.newCachedThreadPool()

Executors.newFixedThreadPool(int nThreads)

초기 Thread 수는 0개, 코어 Thread와 최대 Thread 수는 nThreads 값으로 지정한다.

Thread 개수보다 작업의 개수가 많으면 마찬가지로 Thread를 새로 생성하여 작업을 처리한다. 하지만 작업을 하지 않아도 Thread를 제거하지 않고 놔둔다.

// nThreads = CPU 코어의 수
val executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())

ThreadPoolExecutor를 이용한 Thread Pool 직접 생성

위 3가지 Thread Pool 생성 Method들은 Thread Pool을 내부적으로 알아서 생성한다. Thread Pool 내에서 사용할 Thread의 수를 자동으로 관리하고 싶을 경우에는 ThreadPoolExecutor를 이용하여 커스텀할 수 있다.

val customThreadPoolExecutor = ThreadPoolExecutor(
    3,
    100,
    120L,
    TimeUnit.SECONDS,
    SynchronousQueue<Runnable>()
)

3. Thread Pool 소멸

Thread Pool에 속한 Thread는 Main Thread가 종료되어도 작업 처리를 위해 계속 실행 상태로 남는다. 그렇기 때문에 프로세스가 종료될 때 Thread Pool을 종료시켜 모든 Thread를 종료시켜야 한다.

executorService.shutdown( )

작업 Queue에 남아있는 작업까지 모두 마무리한 후 Thread를 종료한다.

executorService.shutdownNow( )

작업 Queue에 남아있는 작업 잔량에 상관없이 강제 종료한다.

executorService.awaitTermination(long timeout, TimeUnit unit)

모든 작업 처리를 timeout 시간 안에 처리 시 true 반환, 처리하지 못하면 작업 Thread를 interrupt 시키고 false 반환한다.


4. Thread Pool 작업 할당

Thread Pool에 작업을 할당하기 위해서는 어떤 작업이 할당시킬 수 있는지 확인해야 한다. Thread Pool에서 하나의 작업은 Runnable 또는 Callable 객체로 표현한다.

Runnable과 Callable의 차이

Runnable

Runnable은 작업 처리 완료 후 반환값이 존재하지 않으며 Exception을 발생시키지 않는다.

Thread는 Runnable을 인자로 받고 실행할 수 있는데, Runnable의 run( ) Method에 정의된 코드를 수행한다.

Callable

Callable은 작업 처리 완료 후 반환값이 존재하며 Exception을 발생시킨다. Thread는 FutureTask를 통해 Callable을 호출한다. FutureTask.get( )을 통해 Callable의 call( ) Method가 호출되어 결과가 리턴된다.


5. Thread Pool 작업 처리 요청

Thread Pool에서 작업 처리를 요청할 때, ExecutorService의 작업 Queue에 Runnable이나 Callable 객체를 넣어 요청한다. 아래 두 가지 Method를 통해 작업 처리를 요청한다.

execute(Runnable command)

execute( )를 통해 작업 처리를 요청하면 작업 처리 결과는 반환하지 않는다. 만약 작업 처리 도중 예외가 발생하면 Thread가 종료되고 해당 Thread는 Thread Pool에서 제거된다. 코어 Thread 수보다 적은 개수의 Thread가 실행 중인 경우 새 Thread를 시작한다.

submit( )

execute( )는 작업 처리 결과를 반환하지 않는 반면 submit( )은 작업 처리 결과를 Future 타입의 객체로 반환한다. 만약 작업 처리 도중 예외가 발생하면 Thread는 종료되지 않고 다음 작업을 위해 재사용된다. 그렇기 때문에 Thread 생성 오버헤드를 방지하기 위해 submit( )을 가급적 사용한다. 


References

https://codechacha.com/ko/java-callable-vs-runnable/

https://limkydev.tistory.com/55

https://kotlinworld.com/150

https://leveloper.tistory.com/159