Skip to content

item 80 kyunghee

Incheol Jung edited this page Sep 25, 2020 · 1 revision

스레드보다는 실행자, 태스크, 스트림을 애용하라

단순한 작업 큐(work queue)

  • 이 클래스에서는 클라이언트가 요청한 작업을 백그라운드 스레드에 위임해 비동기적으로 처리해줬다.
  • 작업 큐가 필요 없어지면 클라이언트는 큐에 중단을 요청할 수 있고 그러면 큐는 남아 있는 작업을 마저 완료한 후 스스로 종료한다.

동시성 프로그래밍 측면에서 자바는 앞서 나아감

  • 처음 릴리즈된 1996년 부터 스레드, 동기화, wait/notify를 지원
  • 자바 5부터는 동시성 컬렉션인 java.util.concurrent 라이브러리와 Excutor Framework를 지원
  • 자바 7부터는 고성능 병렬 분해 프레임워크인 포크-조인(fork-join) 패키지를 추가
  • 자바 8부터는 parallel 메서드만 한 번 호출하면 파이프라인을 병렬 실행할 수 있는 스트림을 지원

java.util.concurrent 패키지가 등장

  • 이 패키지는 Executor framework 라고 하는 인터페이스 기반의 유연한 테스크 실행 기능을 담고 있다.
  • 모든 면에서 뛰어난 작업 큐를 다음의 단 한줄로 생성할 수 있게 되었다.
  • 자바는 스레드 풀을 생성하고 사용할 수 있도록 java.util.concurrent.ExecutorService 인터페이스와 Executors 클래스 메소드 중 newCachedThreadPool과 newFixedThreadPool 메소드를 제공하고 있다.
ExcutorService exec  = Executors.newSingleThreadExcutor();

// 다음은 이 Excutor에 실행할 태스크(task; 작업)를 넘기는 방법이다.

exec.execute(runnable);

// 다음은 Excutor를 우아하게 종료시키는 방법이다(이 작업이 실패하면 VM 자체가 종료되지 않을 것이다)

exec.shutdown();   
  • ExecutorService의 기능은 이 외에도 많다. 다음은 ExecutorService의 주요 기능

특정 태스크가 완료 되기를 기다린다. (코드 79-2에서 본 get메서드)

태스크 모음 중 아무것 하나 (invokeAny 메서드) 혹은 모든 태스크(invoke All 메서드)가 완료되기를 기다린다.

ExecutorService가 종료하기를 기다린다. (aWaitTermination 메서드).

완료된 태스크들의 결과를 차례로 받는다. (ExecutorCompletionService 이용).

태스크를 특정 시간에 혹은 주기적으로 실행하게 한다. (ScheduledThreadPoolExecutor 이용).

큐를 둘 이상의 스레드가 처리하게 하고 싶다면 간단히 다른 정적 팩터리를 이용하여 다른 종류의 실행자 서비스(스레드 풀)를 생성하면 된다.

스레드 풀의 스레드 개수는 고정할 수도 있고 필요에 따라 늘어나거나 줄어들게 설정할 수도 있다.

우리에게 필요한 실행자 대부분 java.util.concurrent.Executors의 정적 팩터리들을 이용해 생성할 수 있을 것이다. 평범하지 않은 실행자를 원한다면 ThreadPoolExecutor 클래스를 직접 사용해도 된다. 이 클래스로는 스레드 풀 동작을 결정하는 거의 모든 속성을 설정할 수 있다.

  • ExecutorService를 사용하기에 까다로운 애플리케이션도 있다.
  • 작은 프로그램이나 가벼운 서버라면 Executors.newCachedThreadPool이 일반적으로 좋은 선택일 것이다.
  • 일반적으로 특별히 설정할 게 없고 일반적인 용도에 적합하게 동작한다.
  • CachedThreadPool은 무거운 프로덕션 서버에는 좋지 못하다.
  • CachedThreadPool에서는 요청받은 태스크들이 큐에 쌓이지 않고 즉시 스레드에 위임돼 실행된다. 가용한 스레드가 없다면 새로 하나를 생성한다.
  • 서버가 아주 무겁다면 CPU 이용률이 100%로 치닫고 새로운 태스크가 도착하는 족족 또 다른 스레드를 생성하며 상황을 더욱 악화시킨다.

작업 큐를 손수 만드는 일은 삼가야하고 스레드를 직접 다루는 것도 일반적으로 삼가야 한다.

  • 스레드를 직접 다루면 Thread가 작업 단위와 수행 매커니즘 역할을 모두 수행하게 된다.
  • 반면 Executor Framework에서는 작업 단위와 실행 메커니즘이 분리된다.
  • 작업 단위를 나타내는 핵심 추상 개념이 태스크다.

Runnable, Callable (Runnable과 비슷하지만 값을 반환하고 임의의 예외를 던질 수 있다.)

  • Runable 또는 Callable 구현 클래스로 작업을 생성합니다. 둘의 차이는 작업이 끝난 후 리턴 값이 있냐 없느냐의 차이입니다.
  • 태스크를 수행하는 일반적인 메커니즘이 바로 ExcutorService이다. 태스크 수행을 실행자 서비스에 맡기면 원하는 태스크 수행 정책을 선택할 수 있고 생각이 바뀌면 언제든 변경할 수 있다.

자바 7이 되면서 Executor Framework는 Fork-join 태스크를 지원하도록 확장

  1. 포크-조인 태스크는 포크-조인 풀이라는 특별한 실행자 서비스가 실행해준다.
  2. 포크-조인 태스크, 즉 ForkJoinTask의 인스턴스는 작은 하위 태스크로 나뉠 수 있고, ForkJoinPool을 구성하는 스레드들이 이 태스크들을 처리하며 일을 먼저 끝낸 스레드는 다른 스레드의 남은 태스크를 가져와 대신 처리할 수도 있다.
  3. 이렇게하여 모든 스레드가 바쁘게 움직여 CPU를 최대한 활용하면서 높은 처리량과 낮은 지연시간을 달성한다.

핵심 정리

병렬 스트림(아이템 48: 스트림 병렬화)을 이용하면 적은 노력으로 그 이점을 얻을 수 있다.
동시성 프로그래밍을 할 때는 안정성과 응답가능 상태를 유지하기 위해 애써야 한다. (아이템 48) 
Clone this wiki locally