Chapter 6 작업 실행
6.2.3 스레드 풀
스레드 풀 thread pool
은 작업을 처리할 수 있는 동일한 형태의 스레드를풀 pool
의 형태로 관리한다.- 일반적으로 스레드 풀은 풀 내부의 스레드로 처리할 작업을 쌓아둬야 하기 때문에
작업 큐 work queue
와 굉장히 밀접한 관련이 있다. - 작업 스레드는 작업 큐에서 실행할 다음 작업을 가져오고, 작업을 실행하고, 가져와 실행할 다음 작업이 나타날 때까지 대기하는 일을 반복한다.
풀 내부의 스레드를 사용해 작업을 실행하는 방법의 장점
* 매번 스레드를 생성하는 대신 재사용하기 떄문에 스레드를 계속해서 생성할 필요가 없다.
* 여러 개의 요청을 처리하는데 필요한 시스템 자원이 줄어드는 효과가 있다.
* 요청을 처리할 스레드가 이미 만들어진 상태로 대기하고 있기 떄문에 반응속도가 향상된다.
* 스레드 풀의 크기를 적절히 조절해 두면 하드웨어 프로세서가 쉬지 않고 동작하도록 할 수 있다.
* 하드웨어 프로세서가 동작중에 메모리를 전부 소모하거나, 서로 경쟁하느라 성능을 잠식하는 현상도 없다.
미리 정의된 스레드 풀은 다음과 같다.
newFixedThreadPool
: 처리할 작업이 등록되면 그에 따라 실제 작업할 스레드를 하나씩 생성한다. 생성할 수 있는 스레드의 최대 개수는 제한되어 있으며 제한된 개수까지 스레드를 생성하고 나면 더 이상 생성하지 않고 스레드 수를 유지한다.
newCachedThreadPool
: 캐시 스레드 풀은 현재 풀에 갖고 있는 스레드의 수가 처리할 작업의 수보다 많아서 쉬는 스레드가 많이 발생할 때 쉬는 스레드를 종료시켜 훨씬 유연하게 대응할 수 있으며, 처리할 작업의 수가 많아지면 필요한 만큼 스레드를 새로 생성한다. 반면에 스레드의 수는 제한을 두지 않는다.
newSingleThreadExecutor
: 단일 스레드로 동작하는 Executor로서 작업을 처리하는 스레드가 단 하나뿐이다. 만약 작업 중에 Exception이 발생해 비정상적으로 종료되면 새로운 스레드를 하나 생성해 나머지 작업을 실행한다. 등록된 작업은 설정된 큐에서 지정하는 순서 (FIFO, LIFO, 우선순의)
에 따라 반드시 순차적으로 처리된다.
newScheduledThreadPool
: 일정 시간 이후에 실행하거나 주기적으로 작업을 실행할 수 있으며, 스레드의 수가 고정되어 있는 형태의 Executor.Timer 클래스의 기능과 유사하다.
newFixedThreadPool
과 newCachedThreadPool
팩토리 메소드는 일반화된 형태로 구현되어 있는 ThreadPoolExecutor
클래스의 인스턴스를 생성한다. 생성된 ThreadPoolExecutor 인스턴스에 설정 값을 조절해 필요한 형태를 갖추고 사용할 수도 있다.
- Executor는 execute 메소드로 등록해 두면 Executor 내부의 큐에 쌓이고, Executor 내부의 풀에 있는 스레드가 큐에 쌓여있는 작업을 하나씩 뽑아내 처리한다.
- 이처럼 작업별로 스레드를 생성 하는 전략
thread-per-task
에서 풀을 기반으로 하는 전략pool-based
로 변경하면 안정성 측면에서 엄청난 단점을 얻을 수 있다. - 바로 서버에 부하가 걸리더라도 더 이상 메모리가 부족해 죽는 일이 발생하지 않는다는 점이다.
- 부하에 따라 수천 개의 스레드를 생성해 제한된 양의 CPU와 메모리 자원을 서로 사용하려고 경쟁시키는 상황에 이르지 않기 때문에 성능이 떨어질 때도 점진적으로 서서히 떨어지는 특징을 갖는다.
- 또한 Executor를 사용하지 않을때 보다 성능을 튜닝하거나, 실행 과정을 관리하거나, 실행 상태를 모니터링 하거나, 실행 기록을 로그로 남기거나, 오류가 발생했을 때 처리하고자 할 때 여러 가지 방법을 동원해 쉽고 효과적으로 처리하기가 좋다.
6.2.4 Executor 동작 주기
- Executor를 구현하는 클래스는 대부분 작업을 처리하기 위한 스레드를 생성하도록 되어 있다.
- JVM은 모든 스레드가 종료되기 전에는 종료하지 않고 대기하기 때문에 Executor를 제대로 종료시키지 않으면 JVM 자체가 종료되지 않고 대기하기도 한다.
Executor는 작업을 비동기적으로 실행하기 때문에 앞서 실행시켰던 작업의 상태를 특정 시점에 정확하게 파악하기 어렵다.
* 어떤 작업은 이미 완료됐을 수도 있다.
* 몇 개의 작업은 아직 실행 중일 수 있다.
* 다른 작업은 아직 큐에서 대기 상태에 머물러 있을 수도 있다.
- 따라서 종료 과정은 안전한 종료 방법
gracefull
: 작업을 새로 등록하지는 못하고 시작된 모든 작업을 끝낼 때까지 기다림 - 강제적인 종료 방법
abrupt
: 예를 들어 플러그가 빠져 전원이 꺼지는 경우가 있다. - 물론 안전한 종료와 강제적인 종료 사이에 위치시킬 수 있는 여러가지 종료 방법이 있다.
- Executor가 스레드 풀 서비스를 이용해 서비스를 제공한다는 측면에서는 안전한 방법이든 강제적인 종료 방법이든 종료 절차를 밟아야 할 필요가 있다.
- 또 종료 절차를 밟는 동안 실행 중이거나 대기 중이던 작업을 어떻게 처리했는지를 작업을 맡겼던 애플리케이션에게 알려줄 의무가 있다.
- ExecutorService 인터페이스에는 동작 주기를 관리할 수 있는 여러 가지 메소드가 추가되어 있다.
public interface ExecutorService extends Executor {
void shutdown();
List<Runnable> shutdownNow();
boolean isShutdown();
boolean isTerminated();
boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;
// .... 작업을 등록할 수 있는 몇 가지 추가 메소드
}
ExecutorService는 세 가지 동작 주기를 가지고 있다.
`실행 중 running` : ExecutorService를 처음 실행하면 running 상태로 동작한다.
`종료 중 shutting down` : shutdown 메소드를 호출하면 안전한 종료 절차를 진행하며 종료 중 상태로 들어간다.
이 상태에서는 새로운 작업을 등록받지 않으며, 이전에 등록되어 있던 작업까지는 모두 끝마칠 수 있다.
`종료 terminated` : shutdownNow 메소드를 호출하면 강제 종료 절차를 진행한다. 현재 진행 중인 작업도 가능한 취소시키고, 실행되지 않고 대기중이던 작업은 더 이상 실행시키지 않는다.
public class LifecycleWebServer {
private final ExecutorService exec = Executors.newCachedThreadPool();
public void start() throws IOException {
ServerSocket socket = new ServerSocket(80);
while (!exec.isShutdown()) {
try {
final Socket conn = socket.accept();
exec.execute(new Runnable() {
public void run() {
handleRequest(conn);
}
});
} catch (RejectedExecutionException e) {
if (!exec.isShutdown())
log("task submission rejected", e);
}
}
}
public void stop() {
exec.shutdown();
}
private void log(String msg, Exception e) {
Logger.getAnonymousLogger().log(Level.WARNING, msg, e);
}
void handleRequest(Socket connection) {
Request req = readRequest(connection);
if (isShutdownRequest(req))
stop();
else
dispatchRequest(req);
}
interface Request {
}
private Request readRequest(Socket s) {
return null;
}
private void dispatchRequest(Request r) {
}
private boolean isShutdownRequest(Request r) {
return false;
}
}
- LifecycleWebServer는 두 가지 방법으로 종료시킬 수 있다.
- stop 메소드를 호출하거나 클라이언트 측에서 특정한 형태의 HTTP 요청을 전송 할때 종료 된다.
6.2.5 지연 작업, 주기적 작업
- Timer 클래스를 사용하면 특정 시간 이후에 원하는 작업을 실행하는 지연 작업이나 주기적인 작업을 실행할 수 있다.
- 하지만 Timer 클래스는 그 자체로 약간의 단점이 있기 때문에 가능하다면 ScheduledThreadPoolExecutor를 사용하는 방법을 이용하자.
Timer 클래스의 단점
* Timer에 등록된 작업이 너무 오래 실행되면 등록된 다른 TimerTask 작업이 예정된 시각에 실행되지 못할 가능성이 있다.
* TimerTask가 동작 하던 중 예상치 못한 Exception을 던지는 경우 예측하지 못한 상태로 넘어갈 수 있다.
public class OutOfTime {
public static void main(String[] args) throws Exception {
Timer timer = new Timer();
timer.schedule(new ThrowTask(), 1);
SECONDS.sleep(1);
timer.schedule(new ThrowTask(), 1);
SECONDS.sleep(5);
}
static class ThrowTask extends TimerTask {
public void run() { throw new RuntimeException(); }
}
}
- OutOfTime 클래스는 Timer 클래스가 내부적으로 어떻게 꼬일 수 있는지를 보여주고, 혼란은 또 다른 혼란을 낳는다는 말처럼 한 번 문제가 발생하면 작업을 등록하려는 애플리케이션에서 어떤 문제가 발생하는지 보여준다.
- 프로그램 코드만 보자면 6초 동안 실행되다가 종료될 것이라고 예상할 수 있겠지만, 실제로는 1초만 실행되다가
Timer aleady cancelled
라는 메세지의 IllegalStateException을 띄우면서 바로 종료된다. - ScheduledThreadPoolExcutor는 이와 같이 오류가 발생하는 경우를 훨씬 안정적으로 처리해 주기 때문에 자바 5.0 이후에는 일부러 Timer 클래스를 사용할 필요가 없다.
* BlockingQueue를 구현하면서 ScheduledThreadPoolExecutor와 비슷한 기능을 제공하는 DelayQueue 클래스를 사용하자.
* DelayQueue는 큐 내부에 여러개의 Delayed 객체로 작업을 관리하며, 각각의 Delayed 객체는 저마다의 시간을 가지고 있다.
* DelayQueue를 사용하면 Delayed 내부의 시간이 만료된 객체만 take 메소드로 가져갈 수 있다.
* DelayQueue에서 뽑아내는 객체는 객체마다 저장되어 있던 시각 순서대로 정렬되어 뽑아진다.
6.3 병렬로 처리할 만한 작업
- Executor 프레임워크는 실행 정책은 쉽게 지정할 수 있지만, Executor를 사용하려면 실행하려는 작업을 항상 Runnable 인터페이스에 맞춰 구현해야 한다.
- 서버 애플리케이션에서 클라이언트의 요청 한 건을 처리하는 과정에서 병렬화해 처리하는 모습을 볼 수 있다.
- 특히 데이터베이스 서버 같은 경우에 이런 기법을 많이 사용한다.
- 다음 절에서 여러 가지 방법을 사용해 다양한 수준에서 병렬로 동작하는 몇 가지 버전의 컴포넌트를 만들어 보자
6.3.1 예제: 순차적인 페이지 렌더링
public class SingleThreadRenderer {
void renderPage(CharSequence source) {
renderText(source);
List<ImageData> imageData = new ArrayList<ImageData>();
for (ImageInfo imageInfo : scanForImageInfo(source))
imageData.add(imageInfo.downloadImage());
for (ImageData data : imageData)
renderImage(data);
}
}
- 페이지 내용을 순차적으로 렌더링
6.3.2 결과가 나올 때까지 대기: Callable과 Future
- Executor 프레임워크는 작업을 표현하는 방법으로 Runnable 인터페이스를 사용한다.
- run 메소드는 실행이 끝난 다음 뭔가 결과 값을 리턴해 줄 수 없다.
- 예외가 발생할 수 있다고 throws 구문으로 표현할 수도 없다.
- 만약 결과 값을 만들어 냈다면 어딘가 공유된 저장소에 저장해야 한다.
- 오류가 발생 했다면 로그 파일에 오류 내용을 기록하는 정도가 일반적인 처리 방법이다.
결과를 받아올 때까지 시간이 걸리는 작업이 꽤나 많다.
* 데이터베이스에 쿼리를 보내 결과를 받는 경우도 그렇다.
* 네트웍상의 데이터를 받아오는 경우도 그렇다.
* 물론 아주 복잡한 계산을 하는 경우에도 그렇다.
이처럼 결과를 얻는 데 시간이 걸리는 기능은 Runnable 대신 Callable을 사용하는 것이 모양새가 좋다.
Callable 인터페이스에서는 핵심 메소드인 call을 실행하고 나면 결과 값을 돌려받을 수 있으며, Exception도 발생시킬 수 있다. Executor에는 Callable 뿐만 아니라 Runnable이나 java.security.PrivilegedAction 등 여러 가지 유형의 작업을 실행 할 수 있는 기능이 들어 있다.
Runnable과 Callable은 둘 다 어떤 작업을 추상화하기 위한 도구이다.
작업은 일반적으로 유한한 성격을 갖고 있다.
시작하는 지점이 명확하고, 언젠가는 작업이 끝나게 되어 있다.
Executor에서 실행한 작업은
생성 created
,등록 submitted
,실행 started
,종료 completed
와 같은 네 가지의 상태를 통과한다.작업은 상당한 시간 동안 실행됨으로 작업을 중간에 취소할 수 있는 기능이 있어야 한다.
Executor 프레임워크에 먼저 등록 됐지만 시작되지 않은 작업은 언제든지 실행하지 않도록 취소시킬 수 있어야 한다.
이미 시작한 작업은 그 내부 구조가 인터럽트를 처리하도록 잘 만들어져 있는 경우에 한해 취소시킬 수 있다.
Future는 특정 작업이 정상적으로 완료됐는지, 아니면 취소됐는지 등에 대한 정보를 확인할 수 있도록 만들어진 클래스이다.
Future
가 동작하는 사이클에서 염두에 둬야 할 점은 한 번 지나간 상태는 되돌릴 수 없다.이렇게 사이클을 되돌릴 수 없다는 것은 ExecutorService와 동일하다. 일단 완료된 작업은 완료 상태에 영원히 머무른다.
get 메소드의 작업 진행 상태 ( 시작되지 않은 상태, 시작한 상태, 완료된 상태 )
* 작업이 완료 상태에 들어가 있으면 get 메소드는 즉시 결과값을 리턴하거나 Exception을 발생시킨다.
* 아직 작업이 시작하지 않았거나 작업이 실행되고 있는 상태이면 작업이 완료될 때까지 대기한다.
* 작업 실행이 모두 끝난 상태에서 Exception이 발생했다면, get 메소드는 원래 발생했던 Exception을 ExecutionExceptio 예외 클래스에 담는다.
* 작업이 중간에 취소 됐으면 get 메소드에서 CancellationException이 발생한다.
public interface Callable<V> {
V call() throws Exception;
}
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException, CancellationException;
V get(long timeout, TimeUnit unit) throws InterruptedException, ExcutionException, CancellationException, TimeoutException;
}
Callable과 Future 인터페이스
ExecutorService 클래스의 submit 메소드는 모두 Future 인스턴스를 리턴한다.
따라서 Executor에 Runnable이나 Callable을 등록하면 Future 인스턴스를 받을 수 있고 받은 Future 인스턴스를 사용해 작업의 결과를 확인하거나 실행 도중에 작업을 취소할 수도 있다.
아니면 Rannable이나 Callable을 사용해 직접 FutureTask 인스턴스를 생성하는 방법도 있다. ( FutureTask 자체가 Runnable을 상속받고 있기 때문에 Executor에 넘겨 바로 실행시킬 수도 있고, 아니면 run 메소드를 직접 호출해 실행시킬 수도 있다. )
ExecutorService를 구현하는 클래스에서 AbstractExecutorService에 정의된 newTaskFor라는 메소드를 오버라이드할 수 있도록 되어 있으며, newTaskFor를 오버라이드해 등록된 Runnable이나 Callable에 따라 Future를 생성하는 기능에 직접 관여할 수 있다.
protected <T> RunnableFuture<T> newTaskFor(Callable<T> task) {
return new FutureTask<T>(task);
}
- ThreadPoolExecutor.newTaskFor 메소드의 기본 구현 내용
- Executor에 Runnable이나 Callable을 넘겨 등록하는 것은 Runnable이나 Callable을 처음 생성했던 스레드에서 실제 작업을 실행할 스레드로 안전하게 공개하는 과정을 거치도록 되어있다.
6.3.3 예제: Future를 사용해 페이지 렌더링
- 앞서 소개한 순차적 HTML 페이지 렌더링 프로그램의 병렬성을 높여 동작하도록 만들어 보자.
- 먼저 프로그램 내부에서 진행되는 작업을 둘로 나누자.
- 첫 번째는 텍스트를 이미지로 그려내는 작업이다.
- 두 번째는 HTML 페이지에서 사용한 이미지 파일을 다운로드 받는 작업이다.
- 텍스트를 그려넣는 작업은 CPU를 많이 사용하고, 이미지를 다운로드 받는 작업은 I/O 부분을 많이 사용한다.
- 따라서 작업을 이와 같이 둘로 나누면 단일 CPU를 사용하는 시스템에서도 성능을 향상시킬 수 있다.
public abstract class FutureRenderer {
private final ExecutorService executor = Executors.newCachedThreadPool();
void renderPage(CharSequence source) {
final List<ImageInfo> imageInfos = scanForImageInfo(source);
Callable<List<ImageData>> task = new Callable<List<ImageData>>() {
public List<ImageData> call() {
List<ImageData> result = new ArrayList<ImageData>();
for (ImageInfo imageInfo : imageInfos)
result.add(imageInfo.downloadImage());
return result;
}
};
Future<List<ImageData>> future = executor.submit(task);
renderText(source);
try {
List<ImageData> imageData = future.get();
for (ImageData data : imageData)
renderImage(data);
} catch (InterruptedException e) {
// Re-assert the thread's interrupted status
Thread.currentThread().interrupt();
// We don't need the result, so cancel the task too
future.cancel(true);
} catch (ExecutionException e) {
throw launderThrowable(e.getCause());
}
}
interface ImageData {
}
interface ImageInfo {
ImageData downloadImage();
}
abstract void renderText(CharSequence s);
abstract List<ImageInfo> scanForImageInfo(CharSequence s);
abstract void renderImage(ImageData i);
}
- FutureRenderer 클래스에는 먼저 이미지를 다운로드 받는 기능의 Callable을 만들어 ExecutorService에 등록시킨다.
- Callable이 등록되는 즉시 해당하는 작업에 대한 Future 인스턴스를 받을 수 있다.
- 메인 작업이 실행되는 과정에서 이미지 파일을 표현해야 하는 시점이 되면 Future.get 메소드를 호출해 해당하는 이미지 파일을 확보한다.
- 가장 낙관적으로 본다면 get 메소드를 호출하기 전에 이미지를 모두 다운로드했을 것이며, 메인 스레드는 필요한 이미지를 즉시 사용할 수 있다.
- 이상적이지 못한 경우라 해도 이미지를 다운로드 받는 기능이 이미 모두 시작된 상태이기 때문에 순차적인 방법보다 효율적이다.
6.3.4 다양한 형태의 작업을 병렬로 처리하는 경우의 단점
- 주방에서 접시를 닦는 일을 두 명이 처리한다고 한다면, 접시를 닦는 작업은 상당히 효율적인 방법으로 둘로 구분할 수 있다. 한쪽에서는 접시를 닦고, 다른 한쪽에서는 접시를 말리면 된다. 하지만 특정 스레드에 일정한 유형의 작업을 모두 맡겨버리는 정책은 그다지 확장성이 좋지 않다.
- 만약 주방에 일할 사람이 여럿이 추가로 투입되면 작업 방법을 재구성하지 않는 한 모든 사람이 최대한 바쁘게 효과적으로 일 할 수 있도록 만들기가 어렵기 때문이다.
작업을 잘게 쪼개는 의미를 찾으려면 병렬로 처리해서 얻을 수 있는 성능상의 이득이 이와 같은 부하를 훨씬 넘어서야 한다.
- FutureRenderer는 두 종류의 작업을 사용한다.
- 텍스트를 그려 넣는 작업을 처리하고 또 하나는 이미지 파일을 다운로드 받는 작업을 처리한다.
프로그램이 해야 할 일을 작은 작업으로 쪼개 실행할 때 실제적인 성능상의 이점을 얻으려면, 프로그램이 하는 일을 대량의 동일한 작업으로 재정의해 병렬로 처리할 수 있어야 한다.
6.3.5 CompletionService: Executor와 BlockingQueue의 연합
- 처리해야 할 작업을 갖고 있고, 이 작업을 모두 Executor에 등록해 실행시킨 다음 각 작업에서 결과가 나오는 즉시 그 값을 가져다 사용하고자 한다면, 등록한 각 작업별로 Future 객체를 정리해두고, 타임아웃에 0을 지정해 get 메소드로 호출하면 결과가 나왔는지
폴링 polling
해 결과를 찾아올 수 있다. 이러한 방법으로 사용할 수도 있지만, 미리 만들어져 있는 방법이 있다.
CompletionService
- CompletionService는 Executor의 기능과 BlockingQueue의 기능을 하나로 모은 인터페이스다.
- 필요한 Callable 작업을 등록해 실행시킬 수 있고, take나 poll과 같은 큐 메소드를 사용해 작업이 완료되는 순간 완료된 작업의 Future 인스턴스를 받아올 수 있다.
- CompletionService를 구현한 클래스로는 ExecutorCompletionService가 있는데 등록된 작업은 Executor를 통해 실행한다.
private class QueueFuture<V> extends FutureTask<V> {
QueueingFuture(Callable<V> c) { super(c); }
QueueingFuture(Runnable t, V r) { super(t, r); }
protected void done() {
completionQueue.add(this);
}
}
- ExecutorCompletionService에서 사용하는 QueueingFuture 클래스
* ExecutorCompletionService의 구현 내용은 굉장히 직관적이다.
* 생성 메소드에서 완료된 결과 값을 쌓아 둘 BlockingQueue를 생성한다.
* FutureTask에는 done 메소드가 한 번씩 호출 된다.
* 작업을 처음 등록하면 먼저 FutureTask를 상속받은 QueueingFuture라는 클래스로 변환하는데 QueueingFuture의 done 메소드에서는 결과를 BlockingQueue에 추가하도록 되어 있다.
* take와 poll 메소드를 호출하면 그대로 BlockingQueue의 해당 메소드로 넘겨 처리한다.
6.3.6 예제: CompletionService를 활용한 페이지 렌더링
CompletionService를 잘 활용하면 앞서 소개했던 HTML 페이지 렌더링 프로그램의 성능을 두 가지 측면에서 훨씬 개선할 수 있다.
- 전체 실행 시간 단축
- 응답 속도 높임
- 각각 이미지 파일을 다운로드 받는 작업을 생성한다.
이렇게 각각 이미지를 받게 되면 이전 순서대로 다운 받던 부분을 병렬화 하는 것이고, 이미지 파일을 전부 다운로드 받는데 걸리는 전체 시간을 줄일 수 있다. 그리고 다운로드 받은 이미지는 CompletionService를 통해 찾아가도록 하면, 이미지 파일을 다운로드 받는 순간 해당하는 위치에 그림을 그려 넣을 수 있다. 이렇게 구조를 변경하면 사용자 입장에서는 페이지가 동적으로 최대한 빠르게 업데이트 되는 모습을 볼 수 있다.
- CompletionService를 사용해 페이지 구성 요소를 받아오는 즉시 렌더링
public abstract class Renderer {
private final ExecutorService executor;
Renderer(ExecutorService executor) {
this.executor = executor;
}
void renderPage(CharSequence source) {
final List<ImageInfo> info = scanForImageInfo(source);
CompletionService<ImageData> completionService = new ExecutorCompletionService<ImageData>(executor);
for (final ImageInfo imageInfo : info)
completionService.submit(new Callable<ImageData>() {
public ImageData call() {
return imageInfo.downloadImage();
}
});
renderText(source);
try {
for (int t = 0, n = info.size(); t < n; t++) {
Future<ImageData> f = completionService.take();
ImageData imageData = f.get();
renderImage(imageData);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (ExecutionException e) {
throw launderThrowable(e.getCause());
}
}
interface ImageData {
}
interface ImageInfo {
ImageData downloadImage();
}
abstract void renderText(CharSequence s);
abstract List<ImageInfo> scanForImageInfo(CharSequence s);
abstract void renderImage(ImageData i);
}
6.3.7 작업 실행 시간 제한
- 간혹 실행 중인 작업이 일정한 시간이 지난 이후에도 종료되지 않고 결과를 받지 못했다면, 결과를 사용할 시간이 지나 더 이상 작업의 의미가 없을 경우도 있다.
- 이럴 때는 그냥 작업 결과를 버릴 수 밖에 없다.
일정한 시간 이내에만 작업을 처리하도록 만들고자 할 떄 가장 중요한 부분은 결과를 만들어 내지 못하건 결과를 만들어 낼 수 없다고 결론을 내리건 간에 지정된 시간이 지나면 더 이상 기다려 줄 수 없다는 점이다.
타임아웃을 지정할 수 있는 Future.get 메소드를 사용하면 이와 같은 시간 제한 요구사항을 만족할 수 있다. 즉 결과가 나오는 즉시 리턴되는 것은 타임아웃을 지정하지 않는 경우와 같지만, 지정한 시간이 지나도 결과를 만들어 내지 못하면 TimeoutException을 던지면서 실행을 멈추게 되어 있다.
Page renderPageWithAd() throws InterruptedException {
long endNanos = System.nanoTime() + TIME_BUDGET;
Future<Ad> f = exec.submit(new FetchAdTask());
// 광고를 가져오는 작업을 등록했으니, 원래 페이지를 작업한다.
Page page = renderPageBody();
Ad ad;
try {
// 남은 시간 만큼만 대기한다
long timeLeft = endNanos - System.nanoTime();
ad = f.get(timeLeft, NANOSECONDS);
} catch (ExecutionException e) {
ad = DEFAULT_AD;
} catch (TimeoutException e) {
ad = DEFAULT_AD;
f.cancel(true);
}
page.setAd(ad);
return page;
}
- 제한된 시간 안에 광고 가져오기
6.3.8 예제: 여행 예약 포털
사용자가 여행 일자와 필요한 내용을 입력하면 포털 사이트에서 항공사, 호텔, 렌트카 등의 업체가 입력한 입찰 정보를 한군데 모아 보여준다.
업체에 따라 다르지만, 입찰 정보를 받아오는 작업은 이를테면 웹서비스의 형태로 구현되어 있을 수도 있고, 데이터 베이스에 직접 접속해서 받아와야 할 수도 있고, EDI 형태의 트랜잭션을 처리해야 할 수도 있다.
업체별로 입찰 정보를 가져오는 작업은 업체를 단위로 완전히 독립적인 작업이다.
단일 입찰 정보를 가져오는 일이 작업의 단위로써 적절하다고 볼 수 있고, 입찰 정보를 가져오는 작업을 병렬로 처리할 수 있다.
입찰 정보를 가져오는 작업은 n개를 생성해 스레드 풀에 등록하고, 등록한 작업마다 future 객체를 확보하고, 타임아웃을 지정한 get 메소드로 각각의 입찰 정보를 가져오도록 할 수 있다.
게다가 이런 작업을 더 쉽게 만들어 주는 기능이 있는데, 바로 InvokeAll 메소드이다.
invokeAll 메소드가 리턴되면 등록된 모든 작업은 완료되어 결과값을 가지고 있거나 취소되거나 두 가지 상태 가운데 하나이다.
요약
- 애플리케이션을 작업이라는 단위로 구분해 실행할 수 있도록 구조를 잡으면 개발 과정을 간소화하고 병렬성을 확보해 병렬성을 높일 수 있다.
- Executor 프레임워크를 사용하면 작업을 생성하는 부분과 작업을 실행하는 부분을 분리해 실행 정책을 수립할 수 있다.
- 원하는 형태의 실행 정책을 쉽게 만들어 사용할 수 있다.
- 스레드를 직접 사용하는 대신 Executor를 사용해보자.
- 애플리케이션이 하는 일을 개별 작업으로 구분해 처리할 때는 작업의 범위를 적절하게 잡아야 한다.
private class QuoteTask implements Callable<TravelQuote> {
private final TravelCompany company;
private final TravelInfo travelInfo;
...
public TravelQuote call() throws Exception {
return company.solicitQuote(travelInfo);
}
}
public List<TravelQuote> getRankedTravelQuotes {
TravelInfo travelInfo, Set<TravelCompany> companies, Comparator<TravelQuote> ranking, long time, TimeUnit unit) throws InterruptedException {
List<QuoteTask> tasks = new ArrayList<QuoteTask>();
for (TravelCompany compan : companies)
tasks.add(new QuoteTask(company, travelInfo));
List<Future<TravelQuote>> futures = exec.invokeAll(tasks, time, unit);
List<TravelQuote> quotes = new ArrayList<TravelQuote>(tasks.size());
Iterator<QuoteTask> taskIter = tasks.iterator();
for (Future<TravelQuote> f : futures) {
QuoteTask task = taskIter.next();
try {
quotes.add(f.get());
} catch (ExecutionException e) {
quotes.add(task.getFailureQuote(e.getCause()));
} catch (CancellationException e) {
quotes.add(task.getTimeoutQuote(e));
}
}
}
Collections.sort(quotes, ranking);
return quotes;
}
출처 :브라이언 괴츠, 더그 리, 팀 피얼스, 조셉 보우비어, 데이빗 홈즈, 조슈아 블로쉬 공저, 『 멀티코어를 100% 활용하는 자바 병렬 프로그래밍』, 에이콘(2008.7.30), 4장 인용.