본문 바로가기
프로그래밍 놀이터/안드로이드, Java

[Java Concurrency] 작업 실행

by 돼지왕왕돼지 2017. 4. 24.
반응형


 [Java Concurrency] 작업 실행


abrpt, abstractexecutorservice, awaittermination, BlockingQueue, Call, callable, callble, cancellationexception, completion service, completionservice, concurrency, CPU, cpu 대기, Delayed, delayqueue, exception, execution policy, executionexception, executor, executor 설정, executor 프레임워크, executor 프레임웍, executor.timer, executorcompletionservice, executorservice, FIFO, Future, Future.get, GC, Get, getcause, Idle, invokeall, IO, iscancelled, isterminated, iterator, Java, java concurrency, jvm, LIFO, load balacing, new thread, newcachedthreadpool, newfixedthreadpool, newscheduledthreadpool, newsinglethreadexecutor, newtaskfor, OutOfMemoryError, pool-based, producer consumer pattern, rejected execution handler, RejectedExecutionException, Runnable, Running, scheduledthreadpoolexecutor, shutdown, shutting down, start, SUBMIT, Take, terminated, thread pool, thread-per-task, threadpoolexecutor, throws, timeoutexception, timer, timer 클래스 단점, TimerTask, transaction, [Java Concurrency] 작업 실행, 강제 종료, 거절, 경쟁, 관리, 기록, 단일 스레드, 대기, 대량, 독립적인 동작, 딜레이, 로그, 리턴, 메모리, 메모리 부족, 모니터링, 무리, 무시, 무엇을, 반응속도, 병렬, 병렬성, 부하, 부하 분산, 비동기, 상대시각, 상용, 새로운 스레드, 생성, 생성 메소드, 생성 제거, 서버 동작 특성, 서버 하드웨어 자원, 서비스, 성능, 성능상 이점, 순차 실행, 쉬는 스레드 종료, 스레드, 스레드 갯수, 스레드 라이프 사이클, 스레드 수 제한, 스레드 안전성, 스레드 종료, 스레드 풀, 스레드에서 작업 실행, 스메쥴링, 스택 크기, 시각, 시각 만료 객체, 시스템, 시스템 부하, 실행, 실행 거절 핸들러, 실행 과정, 실행 과정 관리, 실행 예정 시각, 실행 정책, 실행 중, 실행 직전, 실행 직후, 싱글 스레드, 안전한 종료방법, 안정성, 앱, 어느 스레드, 어디에서, 어떻게, 언제, 오류, 오버라이드, 완료, 완료 서비스, 우선순위, 우선순위 정책, 운영체제, 응답 속도, 이득, 인터럽트, 인터페이스, 자바 라이브러리, 자바 병렬, 자바6, 자원, 작업, 작업 등록, 작업 라이프 사이클 관리, 작업 상태, 작업 순서, 작업 실행, 작업의 범위, 장점, 재사용, 전원 끄기, 절대시각, 제한 갯수, 종료, 종료 상태, 종료 중, 주기적 호출, 주기적인 작업, 지연, 지연 작업, 취소, 코드, 클래스 메소드, 타임아웃, 통계, 튜닝, 트랜잭션, 팩토리 메소드, 표준적 방법, 프로그램, 프로듀서 컨슈머 패턴, 플랫폼, 한계, 활용, 희생양


-
앱이 해야 할 일을 "작업"이라는 단위로 분할하면 프로그램의 구조를 간결하게 잡을 수 있고,
트랜잭션의 범위를 지정함으로써 오류에 효과적으로 대응할 수 있고, 작업 실행 부분의 병렬성을 자연스럽게 극대화 할 수 있다.



6.1. 스레드에서 작업 실행


-
프로그램에서 일어나는 일을 작업이라는 단위로 재구성하고자 한다면 가장 먼저 해야 할 일은 작업의 범위를 어디까지로 할 것인지 정하는 일이다.
원론적으로 보자면 작업은 완전히 독립적인 동작을 말한다.
독립성이 갖춰져 있어야 병렬성을 보장할 수 있다.
작업을 스케쥴링하거나 부하 분산(load balancing)을 하고자 할 때 폭넓은 유연성을 얻으려면 각 작업이 앱의 전체적인 업무 내용 가운데 충분히 작은 부분을 담당하도록 구성되어야 한다.


-
작업을 실행하는 가장 간단한 방법은 단일 스레드에서 작업 목록을 순차적으로 실행하는 방법이다.
그러나 단일 스레드로 처리하면 스레드가 작업 중에는 어떤 작업이건 대기 상태에 들어가, 처리 시간이 길어질 뿐만 아니라 다른 요청을 전혀 처리하지 못한다.
덧붙여 I/O 작업을 하는 동안 CPU 가 대기하고 있어야 하는 등 서버 하드웨어 자원을 제대로 활용하지 못한다는 문제도 있다.


-
반응속도를 훨씬 높일 수 있는 방법 가운데 하나는 요청이 들어올 때마다 새로운 스레드를 하나씩 만들어 실행시키는 방법이다.
싱글스레드와 비교하여 다음의 차이가 있다.
    1. 작업을 처리하는 기능이 메인 스레드에서 떨어져 나온다. 따라서 서버의 응답 속도가 높아진다.
    2. 여러 작업을 병렬로 처리할 수 있어 두 개 이상의 요청을 받아 동시에 처리할 수 있다.
    3. 실제 작업을 처리하는 스레드의 프로그램은 여러 클라이언트가 접속하는 경우 동시에 동작할 가능성이 높아 스레드 안전성을 확보해야 한다.


-
작업마다 스레드를 생성하는 정책은 상용 서비스에서 사용하기에는 무리가 있다.
특정 상황에서 엄청나게 많은 대량의 스레드가 생성될 수 있는데, 이럴 때 다음의 문제가 발생한다.


    1. 스레드 라이프 사이클 문제
        스레드를 생성하고 제거하는 작업에도 자원이 소모된다.


    2. 자원낭비
        하드웨어에 실제로 장착되어 있는 프로세서보다 많은 수의 스레드가 만들어져 동작하면, 실제로는 대부분의 스레드가 대기(idle)상태에 머문다.
        대기 상태에 머무는 스레드가 많아지면 많아질수록 많은 메모리를 필요로 한다.
        JVM 의 GC 에 가해지는 부하가 늘어날 뿐만 아니라 CPU 사용을 위해 여러 스레드가 경쟁하는 모양이 되기 때문에 메모리 이외에도 많은 자원을 소모한다.
        만약 시스템에 꽂혀 있는 CPU 의 개수에 해당하는 스레드가 동작 중이라면, 스레드를 더 만들어 낸다 해도 성능이 직접적으로 개선되지 않을 수 있으며 오히려 악영향을 미칠 수도 있다.


    3. 안정성 문제
        모든 시스템에는 생성할 수 있는 스레드의 개수가 제한되어 있다.
        제한 개수는 플랫폼, 운영체재마다 다르고, JVM 을 실행할 때 지정하는 인자나 Thread 클래스에 필요한 스택 크기에 따라 달라지기도 한다.
        제한된 양을 모두 사용하고 나면 아마 OutOfMemoryError 가 발생할 것이다.

따라서 일정한 수준까지는 스레드를 추가로 만들어 사용해서 성능상의 이점을 얻을 수 있지만, 특정 수준을 넘어간다면 성능이 떨어지게 마련이다.



6.2. Executor 프레임워크


-
Executor 는 작업 등록과 작업 실행을 분리하는 표준적인 방법이며, 각 작업은 Runnable 형태로 정의한다.
Executor 인터페이스를 구현한 클래스는 작업의 라이프 사이클을 관리하는 기능도 갖고 있고,
몇 가지 통계 값을 뽑아내거나 또는 애플리케이션에서 작업 실행 과정을 관리하고 모니터링하기 위한 기능도 갖고 있다.


-
Executor 의 구조는 프로듀서-컨슈머 패턴에 기반하고 있으며,
작업을 생성해 등록하는 클래스가 프로듀서가 되고, 작업을 실제로 실행하는 스레드가 컨슈머가 되는 모양이다.
일반적으로 프로듀서-컨슈머 패턴을 앱에 적용해 구현할 수 있는 가장 쉬운 방법이 executor 프레임웍을 사용하는 방법이다.

ex)

...

private static final Executor exec = Executors.newFixedThreadPool( NTHREAD );

...

exec.execute( task ); // task 는 Runnable



-
스레드를 직접 생성할 때에는 서버의 동작 특성을 쉽게 변경할 수 없었지만,
Executor 를 사용하면 Executor 의 설정을 변경하는 것만으로 쉽게 변경된다.


-
작업을 등록하는 부분과 실행하는 부분을 서로 분리시켜두면 특정 작업을 실행하고자 할 때
코드를 많이 변경하거나 기타 여러 가지 어려운 상황에 맞닥뜨리지 않으면서도 실행 정책(execution policy)을 언제든지 쉽게 변경할 수 있다는 장점이 있다.


실행 정책은 다음과 같이 "무엇을, 어디에서, 언제, 어떻게" 실행할지를 지정할 수 있다.
    어느 스레드에서 실행할 것인가?
    작업을 어떤 순서로 실행할 것인가? ( FIFO, LIFO, 기타 우선순위 정책 )
    동시에 몇 개의 작업을 병렬로 실행할 것인가?
    최대 몇 개까지의 작업이 큐에서 실행을 대기할 수 있게 할 것인가?
    시스템에 부하가 많이 걸려서 작업을 거절해야 하는 경우, 어떤 작업을 희생양으로 삼아야 할 것이며, 작업을 요청한 프로그램에 어떻게 알려야 할 것인가?
    작업을 실행하기 직전이나 실행한 직후에 어떤 동작이 있어야 하는가?


-
프로그램 어디에서든 간 아래와 같은 코드가 남아 있다면,

new Thread(runnable).start();

조만간 이런 부분에 유연한 실행 정책을 적용할 준비를 해야 할 것이며,
나중을 위해서 Executor 를 사용해 구현하는 방안을 심각하게 고려해봐야 한다.


-
스레드 풀은 이름 그대로 작업을 처리할 수 있는 동일한 형태의 스레드를 풀의 형태로 관리한다.
풀 내부의 스레드를 사용해 작업을 실행하는 방법을 사용하면, 작업별로 매번 스레드를 생성해 처리하는 방법보다 굉장히 많은 장점이 있다.
매번 스레드를 생성하는 대신 이전에 사용했던 스레드를 재사용하기 때문에 스레드를 계속해서 생성할 필요가 없고,
따라서 여러 개의 요청을 처리하는 데 필요한 시스템 자원이 줄어드는 효과가 있다.
작업을 실행하는 데 딜레이가 발생하지 않아 전체적인 반응 속도도 향상된다.


-
스레드 풀의 크기를 적절히 조절해두면 하드웨어 프로세서가 쉬지 않고 동작하도록 할 수 있으며,
하드웨어 프로세서가 바쁘게 동작하는 와중에도 메모리를 전부 소모하거나
여러 스레드가 한정된 자원을 두고 서로 경쟁하느라 성능을 까먹는 현상도 없앨 수 있다.


-
자바 라이브러리에서는 흔히 사용하는 여러 가지 설정 상태에 맞춰 몇 가지 종류의 스레드 풀을 제공한다.

newFixedThreadPool
    처리할 작업이 등록되면 그에 따라 실제 작업할 스레드를 하나씩 생성.
    생성할 수 있는 스레드의 최대 개수는 제한되어 있으며, 제한된 개수까지 스레드를 생성하고 나면
    더 이상 생성하지 않고 스레드 수를 유지한다.
    ( 예외적으로 스레드가 종료되거나 하면 더 생성하기도 한다. )

newCachedThreadPool
    현재 풀에 갖고 있는 스레드의 수가 처리할 작업의 수보다 많아서 쉬는 스레드가 많이 발생할 때 쉬는 스레드를 종료시켜 훨씬 유연하게 대응한다.
    처리할 작업의 수가 많아지면 필요한 만큼 스레드를 새로 생성한다.
    반면 스레드의 수에는 제한을 두지 않는다.

newSingleThreadExecutor
    단일 스레드로 동작하는 Executor 로서 작업을 처리하는 스레드가 단 하나뿐이다.
    만약 작업 중에 Exception 이 발생해 정상적으로 종료되면 새로운 스레드를 하나 생성해 나머지 작업을 실행한다.
    등록된 작업은 설정된 큐에 지정하는 순서(FIFO, LIFO, 우선순위) 에 따라 순차적으로 처리된다.

newScheduledThreadPool
    일정 시간 이후에 실행하거나 주기적으로 작업을 실행할 수 있으며,
    스레드의 수가 고정되어 있는 형태의 Executor.Timer 클래스의 기능과 유사하다.





-
newFixedThreadPool 과 newCachedThreadPool 팩토리 메소드는 일반화된 형태로 구현되어 있는 ThreadPoolExecutor 클래스의 인스턴스를 생성한다.


-
thread-per-task 에서 pool-based 로 변경하면 안정성 측면에서 엄청난 장점을 얻을 수 있다.
웹 서버에 부하가 걸리더라도 더 이상 메모리가 부족해 죽는 일이 발생하지 않는다.
부하에 따라 수천 개의 스레드를 생성해 제한된 양의 CPU 와 메모리 자원을 서로 사용하려고 경쟁시키지도 않는다.
따라서 성능이 떨어질 때도 점진적으로 서서히 떨어진다.
Executor 를 사용하면 사용하지 않을 때보다 성능을 튜닝하거나, 실행 과정을 관리하거나, 실행 상태를 모니터링하거나, 실행 기록을 로그로 남기거나, 오류가 발생했을 때 처리하고자 할 때 여러 가지 방법을 동원해 쉽고 효과적으로 처리하기가 좋다.


-
JVM 은 모든 스레드가 종료되기 전에는 종료하지 않고 대기하기 때문에 Executor 를 제대로 종료시키지 않으면 JVM 자체가 종료되지 않고 대기하기도 한다.


-
Executor 는 작업을 비동기적으로 실행하기 때문에, 그리고 큐와 연결되어 있기 때문에 작업의 상태를 특정 시점에 정확하게 파악하기 어렵다.
앱 종료과정을 보면 안전한 종료방법(graceful, 작업을 새로 등록하지 못하고 시작된 모든 작업이 끝날 때까지 기다림) 이 있고,
또 한편으로는 강제종료 ( abrupt, 전원 끄기 등등 ) 이 있다.
물론 안전한 종료와 강제 종료 사이에 위치시킬 수 있는 여러 가지 종료 방법이 있을 수 있다.


-
서비스를 실행하는 동작 주기와 관련해 Executor 를 상속받은 ExecutorService 인터페이스에는 동작 주기를 관리할 수 있는 여러 메소드가 추가되어 있다.
이와 함께 작업을 등록하는 방법도 몇 가지 더 갖고 있다.

public interface ExecutorService extends Executor{

void shutdown();

List<Runnable> shutdownNow();

boolean isShutdown();

boolean isTerminated();

boolean awaitTermination(long timeout, TimeUnit unit) throws InterrutedException;

...

}



-
내부적으로 ExecutorService 가 갖고 있는 동작 주기에는 실행 중(running), 종료 중(shutting down), 종료(terminated)의 세 가지 상태가 있다.
ExecutorService 를 처음 생성했을 때에는 실행 중 상태로 동작한다.
어느 시점엔가 shutdown 메소드를 실행하면 안전한 종료 절차를 진행하며 종료중 상태로 들어간다.
이 상태에서는 새로운 작업을 등록받지 않으며, 이전에 등록되어 있던 작업(실행되지 않고 대기 중이던 작업도 포함)까지는 모두 끝마칠 수 있다.
shutdownNow 메소드를 실행하면 강제 종료 절차를 진행한다.
현재 진행 중인 작업도 가능한 한 취소시키고, 실행되지 않고 대기 중이던 작업은 더 이상 실행시키지 않는다.


-
ExecutorService 의 하위 클래스인 ThreadPoolExecutor 는 이미 종료 절차가 시작되거나 종료된 이후에 새로운 작업을 등록하려 하면 실행 거절 핸들러(rejected execution handler)를 통해 오류로 처리한다.
실행 거절 핸들러에 따라 다르지만 등록하려 했던 작업을 조용히 무시할 수도 있고, RejectedExecutionException 을 발생시켜 오류로 처리하도록 할 수도 있다.


-
종료 절차가 시작된 이후 실행 중이거나 대기 중이던 작업을 모두 끝내고 나면 ExecutorService 는 종료 상태로 들어간다.
ExecutorService 가 종료 상태로 들어갈 때까지 기다리고자 한다면 awaitTermination 메소드로 대기할 수도 있고,
isTerminated 메소드를 주기적으로 호출해 종료 상태로 들어갔는지 확인할 수도 있다.
일반적으로 shutdown 메소드를 실행한 이후 바로 awaitTermination 을 실행하면 마치 ExecutorService 를 직접 종료시키는 것과 비슷한 효과를 얻을 수있다.


-
자바 라이브러리에 포함된 Timer 클래스를 사용하면 특정 시간 이후에 원하는 작업을 실행하는 지연 작업이나 주기적인 작업을 실행할 수 있다.
하지만 Timer 는 그 자체로 약간의 단점이 있기 때문에 가능하다면 ScehduledThreadPoolExecutor 를 사용하는 방법이 좋겠다.
( Timer 클래스는 상대시각과 절대시각 모두 지원한다. 절대시각을 사용하는 경우 시스템 하드웨어의 시각을 변경시키면 Timer 에 스케쥴된 작업도 함께 변경된다. 하지만 ScehduledThreadPoolExecutor 는 상대 시각만 지원한다는 점에 주의하자.)
ScheduledThreadPoolExecutor 를 생성하려면 직접 ScheduledThreadPoolExecutor 클래스의 생성 메소드를 호출해 생성하는 방법이 있고, 아니면 newScheduledThreadPool 팩토리 메소드를 사용해 생성하는 방법이 있다.


-
Timer 클래스는 등록된 작업을 실행시키는 스레드를 하나만 생성해 사용한다.
만약 Timer 에 등록된 특정 작업이 너무 오래 실행된다면 등록된 다른 TimerTask 작업이 예정된 시각에 실행되지 못할 가능성이 높다.
ScheduledThreadPoolExecutor 를 사용하면 지연 작업과 주기적 작업마다 여러 개의 스레드를 할당해 작업을 실행하느라 각자의 실행 예정 시각을 벗어나는 일이 없도록 조절해준다.


-
Timer 클래스의 또 다른 단점은 TimerTask 가 동작하던 도중 예상치 못한 Exception 을 던져버리는 경우에 예측하지 못한 상태로 넘어갈 수 있다는 점.
Timer 스레드는 예외 상황을 전혀 처리하지 않기 때문에 TimerTask 가 Exception 을 던지면 Timer 스레드 자체가 멈춰 버릴 가능성도 있다.
Timer 클래스는 오류가 발생해 스레드가 종료된 상황에서도 자동으로 새로운 스레드를 생성해주지 않는다.
이런 상황에 다다르면 해당 Timer 에 등록되어 있던 모든 작업이 취소된 상황라고 간주해야 하며,
그 동안 등록됐던 TimerTask 는 전혀 실행되지 않고 물론 새로운 작업을 등록할 수도 없다.


-
만약 특별한 스케줄 방법을 지원하는 스케줄링 서비스를 구현해야 할 필요가 있다면, BlockingQueue 를 구현하면서 ScheduledThreadPoolExecutor 와 비슷한 기능을 제공하는
DelayQueue 클래스를 사용해 보는 것이 좋겠다.
DelayQueue 는 큐 내부에 여러개의 Delayed 객체로 작업을 관리하며, 각각의 Delayed 객체는 저마다의 시각을 갖고 있다.
DelayQueue 를 사용하면 Delayed 내부의 시각이 만료된 객체만 take 메소드로 가져갈 수 있다.
DelayQueue 에서 뽑아내는 객체는 객체마다 지정되어 있던 시각 순서로 정렬되어 뽑아진다.



6.3. 병렬로 처리할 만한 작업


-
Executor 프레임웍에서는 작업을 표현하는 방법으로 Runnable 인터페이스를 사용한다.
Runnable 을 들여다 보면 충분한 기능을 제공하지 못하는 경우가 많다.
run 메소드는 실행이 끝난 다음 뭔가 결과 값을 리턴해 줄 수도 없고,
예외가 발생할 수 있다고 throws 구문으로 표현할 수도 없다.
만약 결과 값을 만들어 냈다면 어딘가 공유된 저장소에 저장해야 하고, 오류가 발생했다면 로그 파일에 오류 내용을 기록하는 정도가 일반적인 처리 방법이다.


-
데이터베이스에 쿼리를 보내는 것, 네트웍에서 데이터를 받는 것, 복잡한 계산하는 하는 것 등 결과를 얻는 데 시간이 걸리는 기능은 Runnable 대신 Callable 을 사용하는 게 모양새가 좋다.
Callable 인터페이스에서는 핵심 메소드인 call 을 실행하고 나면 결과 값을 돌려받을 수 있으며, Exception 도 발생시킬 수 있도록 되어 있다.


-
Executor 프레임웍에서는 먼저 등록됐지만 시작되지 않은 작업은 언제든지 실행하지 않도록 취소시킬 수 있다.
그리고 이미 시작한 작업은 그 내부 구조가 인터럽트를 처리하도록 잘 만들어져 있는 경우에 한해 취소시킬 수 있다.





-
Future 는 특정 작업이 정상적으로 완료됐는지, 아니면 취소됐는지 등에 대한 정보를 확인할 수 있도록 만들어진 클래스이다.
Future 가 동작하는 사이클에서 염두에 둬야 할 점은 한 번 지나간 상태는 되돌릴 수 없다는 것이다.
일단 완료된 작업은 완료 상태에 영원히 머무른다.


-
Future의 get 메소드는 작업이 진행되는 상태에 따라 다른 유형으로 동작한다.
작업이 완료 상태에 들어가 있다면 get 메소드를 호출했을 떄 즉시 결과 값을 리턴하거나 Exception 을 발생시킨다.
반면 아직 작업을 시작하지 않았거나 작업이 실행되고 있는 상태라면 작업이 완료될 때까지 대기한다.
작업 실행이 모두 끝난 상태에서 Exception 이 발생했었다면 get 메소드는 원래 발생했던 Exception 을 ExecutionException 이라는 예외 클래스에 담아 던진다.
작업이 중간에 취소됐다면 get 메소드에서 CancellationException 이 발생한다.

ExcutionException 이 발생한 경우 원래 발생했던 오류는 ExecutionException.getCause 메소드로 확인 가능하다.


-
ExecutorService 클래스의 submit 메소드는 모두 Future 인스턴스를 리턴한다.
Executor에 Runnable 이나 Callable 을 등록하면 Future 인스턴스를 받을 수 있고,
받은 Future 인스턴스를 사용해 작업의 결과를 확인하거나 실행 도중에 작업을 취소할 수도 있다.
아니면 Runnable 이나 Callable 을 사용해 직접 FutureTask 인스턴스를 생성하는 방법도 있다.


-
자바6 부터는 ExecutorService 를 구현하는 클래스에서 AbstractExecutorService에 정의된 newTaskFor 라는 메소드를 오버라이드할 수 있도록 되어 있으며,
newTaskFor 를 오버라이드해 등록된 Runnable 이나 Callable에 따라 Future 를 생성하는 기능에 직접 관여할 수 있다.


-
Future.get 메소드를 감싸고 있는 오류 처리 구문에서는 발생할 수 있는 두 가지 가능성에 모두 대응할 수 있어야 한다.
첫 번째는 Exception 이 발생하는 경우이고, 두 번째는 결과 값을 얻기 전에 get 메소드를 호출해 대기하던 메인 스레드가 인터럽트되는 경우이다.


-
여러 개의 작업 스레드가 하나의 작업을 나눠 실행시킬 때는
항상 작업 스레드 간에 필요한 내용을 조율하는 데 일부 자원을 소모하게 된다.
따라서 작업을 잘게 쪼개는 의미를 찾으려면 병렬로 처리해서 얻을 수 있는 성능상의 이득이 이와 같은 부하를 훨씬 넘어서야 한다.

여러 종류의 작업을 병렬로 처리해 병렬성을 높이고자 노력하는 것은 상당한 양의 업무 부하가 될 수 있지만,
그 업무의 결과로 얻을 수 있는 이득에는 한계가 있음을 알아야 한다.


-
프로그램이 해야 할 일을 작은 작업으로 쪼게 실행할 때 실제적인 성능상의 이점을 얻으려면,
프로그램이 하는 일을 대량의 동일한 작업으로 재정의해 병렬로 처리할 수 있어야 한다.


-
처리해야 할 작업을 갖고 있고, 이 작업을 모두 Executor 에 등록해 실행시킨 다음 각 작업에서 결과가 나오는 즉시 그 값을 가져다 사용하고자 한다면, 등록한 각 작업별로 Future 객체를 정리해두고, 타임아웃에 0을 지정해 get 메소드를 호출하면서 결과가 나왔는지를 폴링해 결과를 찾아올 수 있다.
이런 작업을 위해 미리 만들어져 있는 방법이 완료 서비스(completion service) 이다.


-
CompletionService 는 Executor 의 기능과 BlockingQueue 의 기능을 하나로 모은 인터페이스이다.
필요한 Callable 작업을 등록해 실행시킬 수 있고, take 나 poll 과 같은 큐 메소드를 사용해 작업이 완료되는 순간 완료된 작업의 Future 인스턴스를 받아올 수 있다. ( 정리자주 : CompletionService 에서 작업완료된 녀석이 CompletionQueue(BlockingQueue) 로 제공된다는 이야기 )
CompletionService 를 구현한 클래스로는 ExecutorCompletionService 가 있다.


-
Future.get 에 타임아웃을 적용할 수 있다.
지정한 시간까지 결과를 받아오지 못하면 TimeoutException 을 발생시킨다.


-
ExecutorService 에는 invokeAll 메소드가 있다.
invokeAll 메소드는 작업 객체가 담긴 컬렉션 객체를 넘겨받으며, 그에 해당하는 Future 객체가 담긴 컬렉션 객체를 리턴한다.
invokeAll 메소드는 넘겨받은 작업 컬렉션의 iterator 가 뽑아주는 순서에 따라 결과 컬렉션에 Future 객체를 쌓는다.


시간 제한이 있는 invokeAll 메소드는 등록된 모든 작업이 완료됐거나, 작업을 등록한 스레드에 인터럽트가 걸리거나, 지정된 제한 시간이 지날 때까지 대기하다가 리턴된다.

제한 시간이 지날 때까지 실행 중이던 작업은 모두 실행이 취소된다.

invokeAll 메소드가 리턴되면 등록된 모든 작업은 완료되어 결과값을 가지고 있거나 취소되거나 두 가지 상태 가운데 하나이다.

작업을 등록했던 스레드는 모든 작업을 대상으로 get 메소드를 호출하거나, isCancelled 메소드를 사용해 작업이 완료되거나 취소된 상태를 확인할 수 있다.



요약


-

앱을 작업이라는 단위로 구분해 실행할 수 있도록 구조를 잡으면 개발 과정을 간소화하고 병렬성을 확보해 병렬성을 높일 수 있다.

Executor 프레임웍을 사용하면 작업을 생성하는 부분과 작업을 실행하는 부분을 분리해 실행 정책을 수립할 수 있으며, 원하는 형태의 실행 정책을 쉽게 만들어 사용할 수 있다.

작업을 처리하는 부분에서 스레드를 생성하도록 되어 있다면, 스레드를 직접 사용하는 대신 Executor 를 사용해보자.

앱이 하는 일을 개별 작업으로 구분해 처리할 때는 작업의 범위를 적절하게 잡아야 한다.

웬만한 앱에서는 일반적인 작업 범위가 잘 적용되지만, 일부 앱에서는 스레드를 사용해 병렬로 처리시킨 이득을 보려면 약간의 분석을 통해 병렬로 처리할 작업을 찾아낼 필요가 있다.







반응형

댓글0