실행자 프레임워크(Executor Framework)
- 인터페이스 기반의 유연한 태스크 실행 기능을 담고 있음
- 아래 한 줄로 생성 가능하다.
1
ExecutorService exec = Executors.newSinleThreadExecutor();
- 실행할 테스크(task; 작업)을 넘기고자 한다면 아래와 같이 Runnable 을 구현한 구현체를 넘기면 된다.
1
exec.execute(runnable);
- 만약 실행자를 우아하게 종료하고자 한다면 아래와 같은 코드를 한 줄 실행시키면 된다.(이 작업이 실패하면 VM 자체가 종료 되지 않을 것이다)
1
exec.shutdown();
- 위와 같은 실행자 프레임워크를 사용하여 작업 큐를 손수 만드는일을 삼가야 하고, 스레드를 직접 다루는 것도 일반적으로 삼가야 한다.
- 스레드를 직접 다루면 Thread가 작업 다위와 수행 메커니즘 역할을 모두 수행하게 된다.
- 반면 실행자 프레임워크는 작업 단위와 실행 메커니즘이 분리된다.
- 작업 단위를 나타내는 핵심 추상 개념이 태스크다.
- 태스크에는
Runnable
과Callable
두 가지가 있다.Callable
은 Runnable과 비슷하지만 값을 반환하고 임의의 예외를 던질 수 있다.
- 태스크를 수행하는 일반적인 메커니즘이 바로 실행자 서비스다.
- 태스크 수행을 실행자 서비스에 맡기면 원하는 태스크 수행 정책을 선택할 수 있고, 생각이 바뀌면 언제든 변경할 수 있다.
- 핵심은 (컬렉션 프레임워크가 데이터 모음을 담당하듯) 실행자 프레임워크가 작업 수행을 담당해준다는 것이다.
실행자 서비스의 주요 기능들
실행자 서비스의 주요 기능들은 아래와 같다.
- 특정 태스크가 완료되기를 기다림.
- 태스크 모음 중 아무것 하나(invokeAny 메서드) 혹은 모든 태스크(invokeAll 메서드)가 완료되기를 기다림.
- 실행자 서비스가 종료하기를 기다림(awaitTermination 메서드).
- 완료된 태스크들의 결과를 차례로 받는다(ExecutorCompletionService 이용).
- 태스크를 특정 시간에 혹은 주기적으로 실행하게 한다(ScheduledThreadPoolExecutor 이용).
Note: 만약 큐를 둘 잉상의 스레드가 처리하게 하고 싶다면 간단히 다른 정적 팩터리를 이용하여 다른 종류의 실행자 서비스(스레드 풀)를 생성하면 된다. 스레드 풀의 스레드 개수는 고정할 수도 있고, 필요에 따라 늘어나거나 줄어들게 설정할 수도 있다.
스레드풀
- 개발하면서 필요한 실행자 대부분은
java.util.concurrent.Executors
의 정적 팩터리들을 이용해 생성할 수 있을 것이다. - 평범하지 않은 실행자를 원한다면 ThreadPoolExecutor 클래스를 직접 사용하면 된다.
- 이 클래스로는 스레드 풀 동작을 결정하는 거의 모든 속성을 제어할 수 있습니다.
- 다음은 일반적으로 자주 사용되는 스레드풀들이다.
Executors.newCachedThreadPool
- 일반적으로 작은 프로그램이나 가벼운 서버에 적합하다.
- 특별히 설정할 게 없고 일반적인 용도에 적합하게 동작한다.
CachedThreadPool
- 무거운 프로덕션 서버에선 좋지 못하다!!
- 요청받은 태스크들이 큐에 쌓이지 않고 즉시 스레드에 위임돼 실행되기 때문이다.
- 가용한 스레드가 없다면 새로 하나를 생성하고, 서버가 아주 무겁다면 CPU 이용률은 100%를 치닫고, 새로운 태스크가 도착하는 족족 또 다른 스레드를 생성하며 상황을 더 악화시킬 것이다.
Executors.newFixedThreadPool
- 따라서 무거운 프로덕션 서버에선 스레드 개수를 고정한
newFixedThreadPool
를 사용하거나 완전히 통제할 수 있는ThreadPoolExecutor
를 직접 사용하는 편이 훨씬 낫다.
포크-조인(fork-join)
- 자바7부터 실행자 프레임워크는 포크-조인(fork-join) 태스크를 지원하도록 확장되었다.
포크-조인 태스크
는포크-조인 풀
이라는특별한 실행자 서비스가 실행
해준다.ForkJoinTask
의 인스턴스는 작은 하위 태스크로 나뉠 수 있고, ForkJoinPool을 구성하는 스레드들이 이 태스크들을 처리하며, 일을 먼저 끝낸 스레드는 다른 스레드의 남은 태스크를 가져와 대신 처리할 수도 있다.- 이렇게 하여 모든 스레드가 바쁘게 움직여 CPU를 최대한 활용하면서 높은 처리량과 낮은 지연시간을 달성할 수 있다.
- 이러한 포크-조인 태스크를 직접 작성하고 튜닝하기란 어려운 일지만, 포크-조인 풀을 이용해 만든 병렬 스트림(아이템48)을 이용하면 적은 노력으로 그 이점을 얻을 수 있다.
- 물론 포크-조인에 적합한 작업형태여야 한다.
Note: 실행자 프레임워크의 기능은 이외에도 많은데 ‘자바 병렬 프로그래밍’(에이콘출판사, 2008)를 참고하면 좋다.
추가적으로 스프링에서 제공하는 ThreadPoolTaskExecutor
과 관련해선 여기를 참고하면 좋다.