선생님, 개발을 잘하고 싶어요.

자바 병렬 프로그래밍 - 2부 6장 - 작업 실행 본문

일상/책 리뷰

자바 병렬 프로그래밍 - 2부 6장 - 작업 실행

알고싶은 승민 2022. 6. 17. 08:56
  • 작업(task) 👉 추상적, 명확하게 구분된 업무 단위
  • 애플리케이션 요구 사항을 작업 단위로 분할하면
    • 프로그램 구조 간결
    • 트랜잭션 범위 지정으로 오류 효과적 대응
    • 작업 실행의 병렬성 극대화

스레드에서 작업 실행

  • 작업의 범위를 어디까지로 할 건가 정해야한다.
    • 완전히 독립적인 동작 👉 병렬성 보장을 위함
    • 작업 스케줄링, 부하 분산(load balancing)을 하려면 작업 단위를 충분히 작게 구성

작업을 순차적으로 실행

  • 가장 간단한 방법, 단일 스레드에서 작업 목록을 순차적 실행

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

  • 작업 요청 마다 스레드를 생성한다.
  • 다음과 같은 결과
    • 작업 처리하는 기능이 메인 스레드와 분리 👉 서버의 응답 속도 🆙
    • 동시에 여러 작업을 병렬로 처리할 수 있다 👉 서버의 처리 속도 🆙
    • 프로그램이 동시에 동작할 가능성 ⬆️ 👉 스레드 안전성 필요
  • 요청이 들어오는 속도보다 요청을 더 빨리 처리해야한다.

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

  • 각 작업마다 스레드 생성은 특정 상황에 엄청 많은 스레드를 만들며 다음의 단점이 발생
    • 스레드 라이프 사이클 문제
      • 스레드를 생성하고 제거하는 과정도 자원 소모
      • 작업 부하보다 스레드 생성 부하가 더 클 수 있다.
    • 자원 낭비
      • 실행 중인 스레드는 메모리를 소모한다.
      • CPU 갯수는 한계가 있기에 대부분의 스레드는 idle 상태
      • 대기 상태 스레드 ⬆️ 👉 메모리 필요 🆙
      • CPU를 사용하기 위해 스레드간 경쟁도 자원 소모
    • 안정성 문제
      • 시스템에 따라 최대 스레드 개수 제한
      • OutOfMemory 발생할 수 있다.
  • 따라서
    • 애플리케이션이 만들 수 있는 스레드 수 제한두기
    • 제한된 스레드만으로 동작할 때 너무 많은 요청이 들어오는 상황에도 멈추지 않는지 테스트하기

Executor 프레임웍

  • 작업(task) 👉 논리적인 업무의 단위
  • 스레드 👉 특정 작업을 비동기로 동작시킬 수 있는 방법 제공
  • Executor는 작업 등록과 작업 실행을 분리하는 표준
  • producer-consumer 패턴에 기반
  • 작업을 생성해 등록하는 클래스가 producer
  • 실제로 작업을 실행하는 스레드가 consumer
  • 서버 동작 특성을 Executor 설정을 변경하는 것으로 쉽게 변경 가능

실행 정책(execution policy)

  • 작업 등록과 실행을 분리 👉 실행 정책을 언제든지 쉽게 변경할 수 있다.
    • 작업을 어느 스레드에서 실행?
    • 작업을 어떤 순서로 실행? (FIFO, LIFO, 우선순위)
    • 몇 개까지 병렬 실행?
    • 큐에 최대 몇 개까지 쌓아둘 수 있나?
    • 작업 스케줄링 및 부하 분산은?
    • 작업 실행 직전, 직후에 동작 추가?
  • 실행 정책은 일종의 자원 관리 도구
  • 🌟 프로그램 어디든 직접 스레드를 만들어서 작업을 시작하는 코드가 있다면 Executor 사용을 필히 고려하자.

스레드 풀(thread pool)

  • 스레드 풀 👉 작업을 처리할 수 있는 스레드를 풀 형태로 관리
    • 작업 큐와 밀접한 관련
  • 장점
    • 이전 사용 스레드 재사용 👉 스레드 계속 생성 필요 ❌ 👉 필요한 시스템 자원 ⤵️
    • 스레드가 이미 만들어진 상태로 대기 👉 작업 실행 시 딜레이 ❌ 👉 전체적인 반응 속도 ⤴️
    • 크기가 적절하다면 하드웨어 프로세서가 쉬지 않고 동작 가능
    • 하드웨어는 쉬지 않고 동작하는 동시에, 메모리 전부 소모나 스레드 자원 경쟁 가능성 ⤵️
  • Executors가 기본 제공하는 스레드 풀
    • newFixedThreadPool 👉 스레드 최대 개수 제한
    • newCachedThreadPool 👉 스레드 개수 제한 ❌, 쉬는 스레드를 종료
    • newSingleThreadExecutor 👉 작업이 반드시 큐에 지정된 순서 순차적 처리 보장
    • newScheduledThreadPool 👉 일정 시간 이후 실행, 주기적 작업 실행
  • 풀 기반 전략은 안정성 측면에서 장점을 가진다.
  • 성능이 떨어질 때도 점진적으로 서서히 떨어진다.
  • 성능 튜닝, 실행 과정 관리, 모니터링, 로그 남기기 등 부가 작업 처리 효과적으로 할 수 있다.

Executor 동작 주기

  • JVM은 모든 스레드가 종료되기 전에 종료하지 않고 대기 👉 Executor를 제대로 종료하지 않으면 JVM 자체가 종료되지 않고 대기할수도…
  • 안전한 종료(graceful), 강제 종료(abrupt)
  • ExecutorService 에는 동작 주기 관리 인터페이스 제공
    • shutdown 👉 안전한 종료, 새로운 작업 등록 ❌, 대기 중인 작업 종료까지 기다림
    • shutdownNow 👉 강제 종료, 현재 진행 중인 작업 가능한 취소, 대기 중인 작업 실행 ❌
    • awaitTermination 👉 종료 상태까지 대기
    • isTerminated 👉 종료 상태인가? 쿼리

지연 작업, 주기적 작업

  • Timer 클래스
    • 등록된 작업 실행 스레드 하나 생성 사용
    • 특정 작업이 오래 실행되면 등록된 다른 TimerTask 작업이 예정된 시각에 실행 ❌
  • ScheduledThreadPoolExecutor는 지연 작업, 주기적 작업마다 여러 개의 스레드 할당하므로 실행 예정 시각을 벗어나는 일 ❌

병렬로 처리할 만한 작업

예제: 순차적 페이지 렌더링

  • HTML 페이지에서 텍스트 그린 다음 이미지를 차례로 다운로드 받아 비워둔 공간 채우기

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

  • 결과를 받아올 때 까지 시간이 많이 걸리는 작업
    • 데이터베이스 쿼리
    • 네트워크 데이터 받기
    • 아주 복잡한 계산
  • 결과를 받아서 사용하려면 Callable
  • Runnable, Callable 👉 모두 작업을 추상화
  • Executor에 생성한 작업은
    • 생성 → 등록 → 실행 → 종료 상태를 통과한다.
    • 작업을 중간에 취소할 수 있어야한다.
  • Future 👉 작업의 완료, 취소 정보 확인 가능
    • get 메소드를 통해 대기 가능

예제: Future를 사용해 페이지 렌더링

다양한 형태의 작업을 병렬로 처리하는 경우의 단점

  • 여러 작업을 나눠 실행 👉 작업 스레드간 필요한 내용 조율에 자원 일부 소모
  • 결과적으로 병렬 처리로 얻는 이득이 부하를 훨씬 넘어서야 한다.

CompletionService: Executor와 BlockingQueue의 연합

  • CompletionService 👉 처리해야 할 작업이 있고, 이 작업을 모두 Executor에 등록한 후, 각 작업 결과가 나오는 즉시 그 값을 사용하고자 할 때 사용
    • e.g. 이미지를 여러개 다운로드 받으면서 각 이미지가 다운로드 완료하는 순간에 화면에 랜더링 하고 싶을 때
  • Callable 작업 등록 실행 가능
  • take, poll 같은 큐 메소드 사용해 작업 완료되는 순간 완료된 작업의 Future instance를 받아올 수 있음
  • 특정한 배치 작업을 관리하는 모습을 띤다.

요약

  • 애플리케이션 작업 단위 구분 👉 개발 과정 간소화, 병렬성 ⤴️
  • Executor 프레임워크 👉 작업 생성, 작업 실행 분리해서 실행 정책 수립, 원하는 실행 정책 쉽게 적용 가능
Comments