거꾸로 바라본 세상
반응형

6. 작업실행


대부분  병렬 어플리케이션은 '작업task'을 실행하는 구조가 효율적으로 구성되어있다.

- 작업: 추상적이면서 명확하게 구분된 업무의 단위

작업은 원론적으로 완전히 독립적인 동작으로 상태,결과, 부수효과 등에 영향을 받지 않아야함


작업을 실행하는 가장 간단한 방법

- 단일 스레드에서 작업 목록을 순차적으로 실행하는 방법


6.1.1 작업을 순차적으로 실행

하나의 스레드가 모든 요청을 순차적으로 처리하는 경우

class SingleThreadWebServer {

public static void main(String[] args) {

ServerSocket socket = new ServerSocket(80);

while (true) {

Socket conn = socket.accept();

handleRequest(conn);

}

}

}


* 실제 적용시 성능이 엄청 떨어짐.

단일 스레드에서 IO작업을 하는동안 CPU가 대기하고 있어야하는 경우가 있고 서버 하드웨어 자원을 제대로 활용하지 못하는 문제가 있다.


6.1.2 작업마다 스레드를 직접 생성

- 요청이 들어올 때마다 새로운 스레드를 하나씩 만들어 생성하게 되는 경우


class ThreadPerTaskWebServer {

public static void main(String[] args) {

ServerSocket socket = new ServerSocket(80);

while (true) {

final Socket conn = socket.accept();

Runnable task = new Runnable() {

public void run() {

handleRequest(conn);

}

};

new Thread(task).start();

}

}

}


클라이언트의 요청 내용 처리를 메인스레드에서 처리하는 것이 아니라 새로운 스레드를 생성하여 처리


- 작업을 처리하는 기능이 메인 스레드에서 떨어져 나옴(클라이언트 접속을 기다리는 부분이 빨라짐)

- 동시에 여러 작업을 병렬로 처리할 수 있기 때문에 두 개 이상의 요청을 받아 동시에 처리가능

- 실제 작업을 처리하는 스레드의 안전성을 확봐해야 한다.

- 클라이언트가 접속 요청을 전송하는 속도에 비해 요청을 처리해 응답을 넘겨주는 속도가 빨라야한다.


6.1.3 스레드를 많이 생성할 때 문제점

1) 스레드 라이프 사이클 문제

- 생성과 제거 작업에도 자원이 소모됨

- 클라이언트의 요청을 처리할 때 딜레이가 생기고 JVM과 운영체제도 기초적입 작업을 수행


2) 자원 낭비

- 스레드는 시스템자원, 특히 메모리를 소모함

- 프로세서보다 많은 스레드를 만들어 동작중이라면 대부분의 스레드는 idle 상태에 머무름

- 많은 스레드는 많은 메모리를 필요로함

- GC에 가해지는 부하가 늘어남


3) 안전성 문제

- 모든 시스템에는 생성할 수 있는 스레드의 개수가 제한되어 있음

- Thread 클래스에 필요한 스택의 크기의 제한된 양을 모두 사용하면 OutOfMemoryError 발생하고 해결방법은 별로 없다.

- OutOfMemoryError이 발생하지 않도록 미연에 방지해야함


6,2 Executor 프레임워크

순차 스레드, 각각의 작업을 스레드에서 담아 처리하는 방법에는 문제점이 있다.

순차적인 방법 : 응답속도와 성능이 크게 떨어짐

작업별로 스레드를 생성하는 방법 : 관리측면에서 허점이 있음Thread Pool은 스레드를 관리하는 측면에서 메모리를 모두 소모해 버리지 않도록 통제력을 가지고 있다.


public interface Executor {

void execute(Runnable command);

}


- Executor은 비동기적 작업 실행 프레임워크의 근간을 이루는 인터페이스

- Task Submission(작업등록) 과 Task Execution(작업실행)을 분리

- 각 작업의 라이프 사이클을 관리와 모니터링하는 기능 제공

- 각 작업은 Runnable 형태로 정의

- 일반적으로 Producer-Consumer 패턴에 기반


6.2.1 ex) Executor를 사용한 웹 서버

class TaskExecutionWebServer {

private static final int NTHREADS = 100;

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


public static void main(String[] args) throws IOException {

ServerSocket socket = new ServerSocket(80);

while (true) {

final Socket conn = socket.accept();

Runnable task = new Runnable() {

public void run() {

handleRequest(conn);

}

};

exec.execute(task);

}

}

}


6.2.2 실행정책

- 작업을 등록하는 부분과 실행하는 부분을 서로 분리시켜두면 실행정책(execution policy)를 쉽게 변경할 수 있다. 실행 정책은 '무엇을, 어디에서, 언제, 어떻게' 실행하는지 지정할 수 있다.


1) 작업을 어느 스레드에서 실행할 것인가?

2) 작업을 어떤 순서로 실행할 것인가?(FIFO, LIFO, 기타 다양한 우선순위 정책)

3) 동시에 몇 개의 작업을 병렬로 실행할 것인가?

4) 최대 몇 개까지의 작업이 큐에서 실행을 대기할 수 있게 할 것인가?

5) 시스템 부하가 많이 걸려서 작업을 거절해야할 경우, 어떤 작업을 희생양으로 삼야아 할 것이며, 작업을 요청한 프로그램에 어떻게 알려야 할 것인가.

6) 작업을 실행하기 직전이나 실행한 직후에 어떤 동작이 있어야 하는가?


* 프로그램 어디에서든 간에

 new Thread(runnable).start(); 와 같은 코드가 남아 있다면 이런 부분에 유연한 실행 정책을 적용할 준비를 해야할 것이며, 나중을 위해서 Executor를 사용해 구현하는 방안을 심각하게 고려해봐야 한다.



6.2.3 스레드 풀

- Thread Fool : 스레드 작업을 처리할 수 있는 작업 공간

실행작업

-> 먼저 큐에서 실행할 다음작업을 가져옴

-> 작업을 실행

-> 실행할 다음 작업이 나타날 때까지 대기하는 일을 반복


장점

- 매번 스레드를 생성하는 대신 이전에 사용했던 스래드를 재사용하므로 계속해서 스레드를 생성할 필요 없음

- 여러 개의 요청을 처리하는 데 필요한 시스템 자원이 줄어드는 효과가 있다.

- 크기를 적절히 조절하면 하드웨어 프로세서가 쉬지 않고 동작할 수 있다

- 메모리를 전부 소모하거나 여러 스레드가 한정된 자원을 가지고 서로 경쟁하여 성능을저하는 현상도 없앨 수 있음(메모리가 부족하여 죽는 일이 발생하지 않음)

- 작업별로 스레드를 생성하는 전략(Thread-per-task)에서 풀을 기반으로 하는 전략(pool-based)으로 변경되어 안전성 측면에서 장점을 가지고 있음


자바 스레드풀 메소드

1) newFixedThreadPool

- 처리할 작업이 등록되면 실제 작업할 스레드를 하나씩 생성한다. 

- 생성할 수 있는 스레드의 최대 개수는 제한

- 제한된 개수까지 스레드를 생성하고 나면 더 이상 생성하지 않고 스레드 수를 유지

- 스레드가 작업중에 예외가 발생해서 스레드가 종료되면 하나씩 더 생성

2) newCachedThreadPool

- 현재 Pool에 가지고 있는 스레드의 수가 처리할 작업의 수보다 많아서 쉬는 스레드가 많이 발 생하면 쉬는 스레드를 종료시켜 유연하게 대응

- 처리할 작업의 수가 많아지면 필요한 만큼 스레드를 생성

- 스레드의 수에는 제한을 두지 않음

3) newSingleThreadExecutor

- 단일 스레드로 동작하는 Executor로 작업을 처리하는 스레드가 단 하나

- 작업중 Exception이 발생하여 비정상적으로 종료되면 새로운 스레드를 생성하여 나머지 작업 처리

- 등록된 작업은 설정된 큐에서 지정하는 순서에 따라 순차적으로 처리

4) newScheduledThreadPool

- 일정시간 이후에 실행하거나 주기적으로 작업을 실행

- 스레드의 수가 고정되어 있는 Executor.Timer클래스의 기능과 유사


6.2.4 Executor 동작 주기

- Executor를 구현하는 클래스는 대부분 작업을 처리하기 위한 스레드를 생성하도록 되어있음

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

- 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가 갖고 있는 동작주기

1) 실행중(Running) : ExecutorService를 처음 생성했을 때

2) 종료 중(shutting down) : 어느시점에 shutdown 메소드를 실행하면 안전한 종료 절차를 진행하기 위해 종료중 상태로 전환(새로운 작업을 등록받지 않고, 이전에 등록되어 있는 작업까지는 모두 끝마칠 수 있다) shutdownNow 메소드는 강제종료를 수행

3) 종료(terminated) : 스레드 종료


- ThreadPoolExecutor : 이미 종료된 절차가 시작되거나 종료된 이후에 새로운 작업을 등록하려하면 실행 거절 핸들러 (reject execution handler)를 통해 오류를 처리


클라이언트 측에서 특정한 형태의 HTTP 요청을 전송하는 방법

public class LifecycleWebServer {

private final ExecutorService exec = Executors.newFixedThreadPool(1);

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() {

handlerRequest(conn);

}

});

} catch (Exception e) {

if (!exec.isShutdown()) {

}

}

}

}

public void stop() {

exec.shutdown();

}


6.2.5 지연작업, 주기적 작업

- Timer 클래스는 특정시간 이후에 원하는 작업을 실행하는 지연작업이나 주기적인 작업을 실행할 수 있다. but, Timer 그 자체로는 단점이 있기 때문에 ScheduledThreadPoolExecutor를 사용하는 방법을 추천 (이유 : Timer 클래스는 상대시각과 절대시각도 지원한다. 절대시각을 사용하면 시스템 하드웨어의 시각을 변경시키면 Timer에 스케쥴된 작업도 함께변경됨, 그러나 ScheduledThreadPoolExecutor는 상대시각만 지원)


- 만약 스케줄 방법을 지원하는 스케줄링 서비스를 구현해야 할 필요가 있다면 BlockingQueue를 구현하면서 ScheduledThreadPoolExecutor와 비슷한 기능을 제공하는 DelayQueue 클래스를 사용하는 것을 추천


6.3 병렬로 처리할 만한 작업

6.3.2 결과가 나올때까지 대기 : Callable과  Future

- Runnable은 run메소드의 실행이 끝나면 결과 값을 리턴해 줄 수 없고, 예외가 발생해도 throws 구문으로 표현할 수 없다.

- Callable은 call메소드를 실행하고 결과값을 돌려받을 수 있고 Exception도 발생시키도록 되어 있다.

- Executor에서 실행한 작업은 생성(created), 등록(submitted), 실행(started), 종료(completed) 네 가지 상태를 통과한다.

- Future는 특정 작업이 정상적으로 완료되었는지, 취소되었는지 등에 대한 정보를 확인할 수 있다.


6.3.5 CompletionService : Executor와 BlockingQueue의 연합

- CompletionService는 Executor의 기능과 BlockingQueue의 기능을 하나로 모은 인터페이스이다. 필요한 Callable 작업을 등로해 실행시킬 수 있고, take나 poll과 같은 큐 메소드를 사용해 작업이 완료되는 순간 완료된 작업의 Future 인스턴스를 받아올 수 있다.


public class Renderer {

private final ExecutorService executor;

public 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 im : info) {

completionService.submit(new Callable<ImageData>() {

public ImageData call() throws Exception {

return im.downloadImage();

}

});

}

renderText(source);

try {

for (int t = 0, n= info.size(); t<n; t++) {

Future<ImageData> f = completionService.take();

ImageData data = f.get();

renderImage(data);

}

} catch (Exception e) {

Thread.currentThread().interrupt();

}

}


private void renderText(CharSequence source) {

// TODO Auto-generated method stub

}


private List<ImageInfo> scanForImageInfo(CharSequence source) {

// TODO Auto-generated method stub

return null;

}

}


6.3.7 작업실행 제한 시간

- 일정시간 이내에만 작업을 처리하도록 만들고자 할 때 무한정 그 결과를 기다려줄 수 없다. 그래서 타임아웃을 지정할 수 있는 Future.get() 메소드를 사용하여 해결할 수 있다.



반응형

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

[Java] Lamda Expressions(람다식)  (0) 2023.04.20
[Java] Generic(제네릭)  (0) 2023.04.20
volatile 키워드  (0) 2017.04.13
NIO TCP blocking channel 소켓서버 & 클라이언트  (0) 2017.03.30
비동기 파일채널  (0) 2017.03.29
profile

거꾸로 바라본 세상

@란지에。

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!