Java/Executor Service

ExecutorService란?

Junuuu 2023. 10. 19. 00:01
728x90

Executor란?

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

ExecutorService가 Executor 인터페이스를 구현하고 있습니다.

Executor 인터페이스는 Runnable 객체를 미래의 어느 시점에 주어진 명령으로 실행합니다.

 

Runnable이란?

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

Runnable 인터페이스는 인스턴스가 스레드에 의해 실행되는 의도된 모든 클래스에서 구현합니다.

예를 들어 Runnable 인터페이스는 Thread 클래스가 구현하고 있습니다.

스레드를 시작하면 별도의 실행 중인 스레드에서 객체의 run 메서드가 호출됩니다.

 

 

ExecutorService란?

public interface ExecutorService extends Executor {
    void shutdown();
    List<Runnable> shutdownNow();
    boolean isShutdown();
    boolean isTerminated();
    //위는 라이프 사이클 관리를 위한 기능
    
    //반환값을 받기 위해 Callable으로 실행시키고 Future로 반환
    <T> Future<T> submit(Callable<T> task);
    //Runnable은 반환값이 없음, Future의 get 메서드는 작업이 성공적으로 완료되면 null을 반환
    Future<?> submit(Runnable task);
    
    ...
    //주어진 작업을 동시에 모두 실행하고 각각의 상태와 결과를 갖는 Future 객체를 List로 반환, 
    //모든 결과가 반환될 때 까지 대기하는 Blokcing 방식
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
        throws InterruptedException;
    
    //주어진 작업을 실행하여 성공적으로 완료된 작업의 결과를 반환
    //정상 또는 예외 반환시 완료되지 않은 작업은 취소
    <T> T invokeAny(Collection<? extends Callable<T>> tasks)
        throws InterruptedException,
}

 

ExecutorService란 Java에서 동시 및 병렬 프로그래밍을 간소화하기 위해 제공되며 스레드를 직접 생성하고 관리하지 않고 이를 고수준으로 추상화하여 제공합니다.

 

Executor를 구현하고 있는 인터페이스이며 그 외의 여러 가지 메서드들이 추가되었습니다.

submit 메서드를 보면 Callable객체의 task를 넘기고 Future 객체로 반환받습니다.

(Future, Callable)등의 객체가 추가로 등장합니다.

 

Executors 클래스를 제공하여 ExecutorService에 대한 팩토리 메서드를 제공합니다.

 

Callable 인터페이스

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

스레드에서 실행될 수 있다는 점이 Runnable과 유사하지만 void를 반환하는 Runnable 인터페이스와는 다르게 결과를 반환하고 예외를 발생시킬 수 있습니다.

 

Future 인터페이스

public interface Future<V> {
    //작업 취소
    boolean cancel(boolean mayInterruptIfRunning);
    //작업이 취소되었는지 확인
    boolean isCancelled();
    //작업이 끝났는지 확인
    boolean isDone();

    //get을 통해 블로킹방식으로 결과를 가져올 수 있으며, 타임아웃 설정도 가능합니다.
    V get() throws InterruptedException, ExecutionException;

    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

비동기 계산의 결과를 나타내는 객체입니다.

 

Callable 인터페이스의 구현체인 작업은 가용 가능한 쓰레드가 존재하지 않은 경우에 실행이 미뤄질 수 있고, 작업 시간이 오래 걸릴 수 있습니다.

 

그래서 실행 결과를 바로 받지 못하고 미래의 어느 시점에 얻을 수 있는데, 미래에 완료된 Callable의 반환값을 구하기 위해 사용됩니다.

 

Executors 클래스

public class Executors {

	//고정된 수의 n개의 쓰레드를 재사용하는쓰레드풀을 생성합니다.
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
    ...
}

팩토리 클래스인 Executors 클래스가 Executor, ExecutorService를 구현할 스레드풀을 static method를 활용하여 생성해 줍니다.

사용하는 입장에서는 스레드의 개수만 넘겨주면 됩니다.

 

newFixedThreadPool 실습

@Component
class FixedThreadPool : ApplicationRunner {
    override fun run(args: ApplicationArguments) {
        val executorService: ExecutorService = newFixedThreadPool(2)
        logger.info {"FixedThreadPool start"}
        val myTaskProcessOneSeconds = {
            println("1초가 걸립니다.")
            sleep(1000)
        }
        //1초가 걸리는 작업을 3번 수행, 쓰레드풀의 크기는 2이다.
        //최소 2초가 걸려야 한다.
        executorService.submit(myTaskProcessOneSeconds)
        executorService.submit(myTaskProcessOneSeconds)
        executorService.submit(myTaskProcessOneSeconds)
        logger.info {"FixedThreadPool end"}
    }
}

ThreadPool의 크기가 2인 ExecutorService를 만들고 1초가 걸리는 Task를 수행해 보았습니다.

총 3번의 submit이 이루어졌고 스레드풀의 크기는 2이기 때문에 최소 2초가 걸려야 합니다.

 

2023-09-30T00:58:14.418+09:00  INFO 2016 --- [pool-6-thread-1] com.example.study.log.Logging            : 1초가 걸립니다.
2023-09-30T00:58:14.418+09:00  INFO 2016 --- [pool-6-thread-2] com.example.study.log.Logging            : 1초가 걸립니다.
2023-09-30T00:58:15.423+09:00  INFO 2016 --- [pool-6-thread-1] com.example.study.log.Logging            : 1초가 걸립니다.

14초에 두 번, 15초에 나머지 작업이 한번 수행된 것을 로깅으로 확인할 수 있습니다.

 

 

마무리

ExecutorService를 활용하여 병렬 프로그래밍을 추상화해서 사용할 수 있다.

Executors 팩토리 클래스를 통해 쓰레드 풀을 편리하게 사용할 수 있다.

호출은 Runnable, Callable을 통해 이루어지고 Callable은 반환타입이 존재한다.

Future를 통해 비동기로 작업이 수행되는 콜백도 받을 수 있다.