자바 병렬 프로그래밍 - 2부

Intro

자바 병렬 프로그래밍은 총 4부로 이루어져 있으며 1부는 “이론”, 2~4부가 “실제” 위주이다. 특히 4부는 “고급 주제”이다.

해당 책의 내용을 자세히 작성할 생각은 없고 기억할 부분만 작성한다.

따라서 자세한 내용은 잘 정리하신 분의 블로그 + 책을 참고하자.


2부에서는 무엇을 배울까?

  • 스레드를 어떻게 사용하면 병렬 앱의 성능과 응답성을 높이는가
  • 병렬화할 수 있는 작업을 구분 방법과 작업 실행 프레임웍에서 실행시키는 방법
  • 작업과 스레드를 중간에 종료시키는 방법 -> 이것을 잘해야 매우 안정적이다 할 수 있음
  • 작업 실행 프레임웍의 고급 기능 및 GUI에서의 응답성 향상 방법



2부 - 병렬 프로그램 구조 잡기

요약

image
image image image



작업 실행

작업(task)은 업무의 단위 -> 즉, 추상적이면서 명확히 구분되는 단위 -> 병렬성을 극대화

  • EX) 클라이언트 요청 = 작업 1개
  • 다만, 클라이언트 요청 1건의 세부 내용을 또 병렬 처리하는 경우도 많다.
    • ex:아래에서 소개한 페이지 렌더링 관련


스레드를 많이 생성할 때의 문제점 -> 해결법으로 스레드 풀을 사용하자

  • 스레드 라이프 사이클의 문제점 -> 생성 제거에도 만은 자원 소모

  • 자원 낭비 -> 메모리 소모, 대기 스레드 많을수록 경쟁 상승, CPU 코어보다 많을수록 악영향 가능성 등등
  • 안정성 문제 -> 만약 스택 크기를 넘어서는 스레드 양이면 OutofMem… 에러 등등


Executor 프레임웍은 강력한 비동기 작업 실행 인터페이스이고, 프로듀서-컨슈머 패턴 기반 -> Thread 대신 사용해보자!!

  • newFixedThreadPool : 스레드 수 제한O
  • newCachedThreadPool : 스레드 수 제한X, 쉬는 스레드는 종료
  • newSingleThreadExecutor : 단일 스레드로 작업 처리. 순차적 처리
  • newScheduledThreadPool : Timer 대신 사용 추천


JVM은 모든 스레드가 종료되지 않으면 대기

  • Executor는 비동기라서 종료 시점을 알기 어려움 -> 따라서 Executor를 상속받는 ExecutorService 인터페이스 활용해서 해결 (종료 관련 메소드 제공. ex:shutdown )


Callable + Future 사용한 병렬 렌더링 예시 -> 동작할 렌더링은 이미지 다운 및 출력과 텍스트 출력

  • Callable을 이미지 다운 로직으로 만들어 ExecutorService에 등록(submit) 즉시 해당 작업의 Future 객체를 반환받을 수 있게 됨.
  • 반환 받을 땐 Future.get 메소드로 받으며, 만약 아직 동작 중(이미지 다운)이면 대기한다.


CompletionService 사용한 병렬 렌더링 예시 -> 동작할 렌더링은 이미지 다운 및 출력과 텍스트 출력

  • 다운 로직에 이를 적용하면 다운받는 순간 해당 위치에 이미지를 바로 출력할 수 있다.
  • 즉, 작업이 완료된 즉시 결과값을 사용할 때 유용하다.
  • 위와 같이 Callablesubmit으로 등록 후 (이때, 이미지 다운 시작) 블로킹 큐를 사용하기 때문에 take 메소드로 다운 완료되었으면 즉시 결과값을 가져와 이미지를 출력한다.



중단 및 종료

모든 스레드는 boolean 타입으로 인터럽트 상태를 가진다.

인터럽트란 특정 스레드에게 작업 중단을 요청 -> 단지, 요청이라는 걸 기억하자

스레드 중단은 “협력적인 방법”을 권장

  • 기본 방식 : “플래그(취소 요청)”를 주기적 확인 -> 해당 변수는 volatile 권장
  • 물론, 우리는 “인터럽트”를 사용하자
    • 대부분 블로킹 메소드에선 InterruptedException을 던져주니 이것으로 처리하자
    • 예로 Future.get 메소드에서 InterruptedException이면 Future.cancel 메소드로 인터럽트 요청하자


데몬 스레드 : JVM 내부적으로 사용 위해 실행하는 스레드



스레드 풀 활용

스레드 풀은 “동일, 독립, 다수 작업”에 효과적

Executor가 유연성을 가지지만 특정 형태에서 실행할 수 없는 작업들이 있다.

  • 의존성이 있는 작업
  • 스레드 한정 기법 사용한 작업
  • 응답 시간 민감한 작업
  • ThreadLocal 사용한 작업 : Executor는 스레드 최대한 재사용, ThreadLocal은 사용한 각 버전으로 스레드가 유지


스레드 풀은 너무 크거나 작으면 안 된다. 풀의 크기 산정 방식은??

  • 하드웨어에 CPU가 몇 개 꽂혀 있는지, 실행 작업이 CPU 연산이 많은지 아니면 I/O 연산이 많은지 등등..
  • CPU 연산 많은 경우 -> CPU N개 코어라면 N+1 크기가 최적이라 알려져 있다.
  • I/O 연산 많은 경우 -> 크기를 더 크게 잡아야 한다.
  • 각 작업 하나당 db를 쓴다고 하면 스레드 풀의 실제 크기는 db 커넥션 풀의 크기로 제한


ThreadPoolExecutor는 Executor에 대한 기본 내용이 구현되어 있는 클래스이다. 다양한 옵션이 있다.

  • corePoolSize : 스레드의 개수
  • maximumPoolSize : 동시 실행 스레드 최대 수
  • keepAliveTime : 대기하는 스레드 최대 유지 시간
    • 예로 newCachedThreadPool의 경우 maximumPoolSize가 Integer.MaxValue이고, keepAliveTime이 1분 설정을 가지기 때문에 위에서 언급한 특징을 가지는 것
  • 또한, beforeExecute -> afterExecute -> terminated 순으로 흐르는 훅도 제공한다.


스레드 팩토리란 스레드 풀에서 새로운 스레드를 생성하는 곳이다.



GUI 애플리케이션

대부분의 GUI 프레임워크는 단일 스레드 이벤트 큐 모델

스윙(swing)의 단일 스레드 규칙 : 스윙 컴포넌트와 모델 객체는 이벤트 스레드 내부에서만 생성, 변경, 사용할 수 있다.

  • 시간이 짧게 걸리는 GUI 작업
    • 예로 이벤트 발생 시 GUI 프레임웍에서 발생 -> 앱으로 전달한다. 즉, 통제권이 이벤트 스레드 내부에서만 동작
  • 시간이 길게 걸리는 GUI 작업
    • 독립된 스레드에서 실행 -> newCachedThreadPool 메소드가 딱이다.

댓글남기기