작업 실행

작업(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)

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를 받아올 수 있음
특정한 배치 작업을 관리하는 모습을 띤다.

Last updated