IT_Programming/Java

일시적 태스크 실행시에 쓰레드 풀링 이용하기

JJun ™ 2007. 11. 22. 02:02
많은 일시적 태스크의 실행을 위한 프로그램을 개발하고 있다면, 쓰레드 풀링(thread pooling)이라는 테크닉을 사용하는 것이 유용할 것이다. 각각의 새로운 태스크에 대해서 쓰레드를 생성했다가 그 태스크가 끝난 후 버리는 대신, 쓰레드의 풀(pool)을 생성하고 그 풀로 하여금 각 태스크를 실행하게 하는 것이다. 풀의 쓰레드가 가능한 상태가 되면 태스크는 즉시 실행되며, 태스트가 끝난 후 쓰레드는 풀로 다시 리턴된다. 아니면, 태스크는 쓰레드가 가능한 상태가 될 때까지 대기한다.

J2SE 5.0에서는 새로운 java.util.concurrent 패키지를 제공하며, 그 안에는 미리 구축된 쓰레드 풀링 프레임워크를 제공하는 병행 유틸리티가 포함되어 있다. java.util.concurrent의 execute인터페이스는 다음과 같이 Runnable오브젝트를 허용하는 단일 메서드인 execute를 제공한다.

public interface Executor {
    public void execute(Runnable command);
}

쓰레드 풀링 프레임워크를 이용하기 위해 Executor 인스턴스를 생성하고 구동 가능한 태스크에 전달하자.

Executor executor = ...;
executor.execute(aRunnable1);
executor.execute(aRunnable2);

그 후 Executor 인터페이스의 구현을 찾거나 생성한다. 이 구현은 새로운 쓰레드에서 태스크를 즉시 구동하거나 연속적으로 구동할 수 있다. 예를 들어, 다음은 각 태스크에 대한 새로운 쓰레드를 산출하는 구현이다.

class MyExecutor implements Executor {
    public void execute(Runnable r) {
        new Thread(r).start();
    }
}

병행 유틸리티에는 또한 많은 일반적 풀링 작용에 대한 지원을 제공하는 ThreadPoolExecutor 클래스가 포함되어있다. 네 개의 ThreadPoolExecutor 컨스트럭터 중 하나를 이용하여 풀의 크기, 지속시간, 쓰레드 팩토리, 거부된 쓰레드에 대한 핸들러 등의 옵션을 지정할 수 있다.

public ThreadPoolExecutor(int corePoolSize,
                                int maximumPoolSize,
                                long keepAliveTime,
                                TimeUnit unit,
                                BlockingQueue workQueue);

public
ThreadPoolExecutor(int corePoolSize,
                                int maximumPoolSize,
                                long keepAliveTime,
                                TimeUnit unit,
                                BlockingQueue workQueue,
                                ThreadFactory threadFactory);

public
ThreadPoolExecutor(int corePoolSize,
                                int maximumPoolSize,
                                long keepAliveTime,
                                TimeUnit unit,
                                BlockingQueue workQueue,
                                RejectedExecutionHandler handler);

public
ThreadPoolExecutor(int corePoolSize,
                                int maximumPoolSize,
                                long keepAliveTime,
                                TimeUnit unit,
                                BlockingQueue workQueue,
                                ThreadFactory threadFactory,
                                RejectedExecutionHandler handler);

그러나 컨스트럭터를 사용자가 직접 호출할 필요는 없다. 대신에 java.util.concurrentExecutors 클래스가 쓰레드 풀을 호출해준다. 간단한 케이스에서는 사용자가 Executors 클래스의 newFixedThreadPool 메서드를 호출하고 풀 안에서 원하는 몇가지 쓰레드들을 전달한다. 그런 다음 Executor를 확장하는 인터페이스인 ExecutorService 를 이용하여 Runnable 태스크를 실행하던지, 결과를 제출한다. ExecutorService 의 submit 메서드를 호출함으로서 결과를 받을 수 있게 된다. submit 메서드는 또한 태스크가 완료됐는지 확인할 때 사용하는 Future도 리턴한다.

테스트 프로그램을 구동시켜 쓰레드 풀 사용의 데모를 보기로 하자. 첫번째로 다음 프로그램, NamePrinter에서는 언제 태스크를 시작하고, 잠시 멈추는지, 또 언제 완료되는지 알려준다.

public class NamePrinter implements Runnable {
    private final String name;
    private final int delay;

    public
NamePrinter(String name, int delay) {
        this.name = name;
        this.delay = delay;
    }

    public
void run() {
        System.out.println("Starting: " + name);
          try {
            Thread.sleep(delay);
        } catch (InterruptedException ignored) { }
        System.out.println("Done with: " + name);
    }
}

다음은 테스트 프로그램 UsePool이다. 싸이즈 3의 쓰레드 풀을 생성하고 10개의 태스크(즉 NamePrinter를 10번 구동)를 추가한다. UsePool 프로그램은 그 후 태스크가 끝나면 shutdown과 awaitTermination를 호출한다. 즉각적인 shutdown을 시도하는 shutdownNow 메서드도 있다. 이 메서드를 사용하면 shutdown메서드를 사용하는 것보다도 빠르게 종료된다. shutdownNow 메서드는 남아있는 Runnable태스크들의 List를 리턴한다.

import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class UsePool {
    public static void main(String args[]) {
        Random random = new Random();
        ExecutorService executor = Executors.newFixedThreadPool(3);

        // Sum up wait times to know when to shutdown
        int waitTime = 500;
        for (int i = 0; i < 10; i++) {
            String name = "NamePrinter " + i;
            int time = random.nextInt(1000);
            waitTime += time;
            Runnable runner = new NamePrinter(name, time);
            System.out.println("Adding: " + name + " / " + time);
            executor.execute(runner);
        }

        try {
            Thread.sleep(waitTime);
            executor.shutdown();
            executor.awaitTermination(waitTime, TimeUnit.MILLISECONDS);
        } catch (InterruptedException ignored) {}

        System.exit(0);
    }
}

NamePrinter와 UsePool를 컴파일하고 UsePool을 구동하자. 다음은 샘플 결과를 구동한 것이다. 각각의 구동은 무작위적인 휴식이 있는 유일한 것임을 상기하기 바란다.

   Adding: NamePrinter 0 / 30
   Adding: NamePrinter 1 / 727
   Adding: NamePrinter 2 / 980
   Starting: NamePrinter 0
   Starting: NamePrinter 1
   Starting: NamePrinter 2
   Adding: NamePrinter 3 / 409
   Adding: NamePrinter 4 / 49
   Adding: NamePrinter 5 / 802
   Adding: NamePrinter 6 / 211
   Adding: NamePrinter 7 / 459
   Adding: NamePrinter 8 / 994
   Adding: NamePrinter 9 / 459  
   Done with: NamePrinter 0
   Starting: NamePrinter 3
   Done with: NamePrinter 3
   Starting: NamePrinter 4
   Done with: NamePrinter 4
   Starting: NamePrinter 5
   Done with: NamePrinter 1
   Starting: NamePrinter 6
   Done with: NamePrinter 6
   Starting: NamePrinter 7
   Done with: NamePrinter 2
   Starting: NamePrinter 8
   Done with: NamePrinter 5
   Starting: NamePrinter 9
   Done with: NamePrinter 7
   Done with: NamePrinter 9
   Done with: NamePrinter 8

처음 세개의 NamePrinter 오브젝트가 빨리 시작되는 것들이다. 실행되고 있는 각각의 NamePrinter 오브젝트가 끝나면 다음 NamePrinter 오브젝트가 시작된다.

J2SE 5.0에서는 더욱 더 많은 쓰레드 풀링 프레임워크 기능들이 있다. 예를 들어 태스크를 "예약"하여 실행할 수 있는 쓰레드 풀의 스케줄을 작성할 수도 있다.

쓰레드 풀과 새로운 병행 유틸리티들에 대한 좀 더 많은 정보는 Concurrency Utilities를 참조하기 바란다.

'IT_Programming > Java' 카테고리의 다른 글

JavaTM Web Start  (0) 2008.01.23
[Tip] Java Main Class를 .exe파일로 실행시키는 방법  (0) 2008.01.22
signed applet 만들기  (0) 2007.11.22
Applet → Refference JavaScript  (0) 2007.11.22
[JDBC] CallableStatement (Stored procedure)  (0) 2007.10.28