csct3434
Fork Join Framework: CPU bounded vs I/O bounded 작업 효율성 비교 본문
1. Fork Join Framework의 개요
Fork Join Framework는 Java 7에서 도입된 병렬 처리 프레임워크로, '분할 정복(divide and conquer)' 알고리즘을 효과적으로 구현할 수 있게 해준다. 이 프레임워크의 핵심 아이디어는 큰 작업을 작은 단위로 재귀적으로 분할(fork)하고, 각각의 결과를 합쳐(join) 최종 결과를 얻는 방식이다.
주요 구성 요소
- ForkJoinPool: 작업 스케줄링과 실행을 관리하는 특수한 ExecutorService
- ForkJoinTask: 풀에서 실행되는 기본 태스크 타입 (RecursiveAction, RecursiveTask의 상위 클래스)
- Work-Stealing 알고리즘: 유휴 스레드가 다른 바쁜 스레드의 작업 큐에서 작업을 '훔쳐와' 처리하는 메커니즘
public class SumTask extends RecursiveTask<Long> {
private final long[] numbers;
private final int start;
private final int end;
private final int threshold = 10_000;
// 생성자 및 기타 메서드 생략
@Override
protected Long compute() {
int length = end - start;
if (length <= threshold) {
// 직접 계산할 만큼 작업이 작아지면 계산 수행
return computeDirectly();
}
// 작업 분할
int middle = start + length / 2;
SumTask leftTask = new SumTask(numbers, start, middle);
SumTask rightTask = new SumTask(numbers, middle, end);
// 왼쪽 작업은 비동기로 실행
leftTask.fork();
// 오른쪽 작업은 현재 스레드에서 실행
Long rightResult = rightTask.compute();
// 왼쪽 작업 결과 대기 및 합산
Long leftResult = leftTask.join();
return leftResult + rightResult;
}
}
2. CPU Bounded vs I/O Bounded 작업의 특성
CPU Bounded 작업
- 계산 집약적인 작업으로, 프로세서의 연산 능력에 의해 성능이 좌우됨
- 예: 수학적 계산, 정렬, 검색, 이미지 처리 등
- 특징: CPU 사용률이 높고, 작업 중 스레드 차단(blocking)이 거의 발생하지 않음
I/O Bounded 작업
- 외부 리소스와의 상호작용이 많은 작업으로, I/O 작업의 속도에 의해 성능이 좌우됨
- 예: 파일 읽기/쓰기, 네트워크 통신, 데이터베이스 쿼리 등
- 특징: CPU 사용률이 낮고, 작업 중 스레드가 자주 차단됨(I/O 완료 대기)
3. Fork Join Framework의 설계 특성
기본 스레드 풀 크기
Fork Join Framework의 기본 스레드 풀 크기는 일반적으로 시스템의 가용 프로세서 수와 동일하다:
// 기본 생성자는 Runtime.getRuntime().availableProcessors()를 사용
ForkJoinPool pool = new ForkJoinPool();
// 명시적으로 스레드 수를 지정할 수도 있음
ForkJoinPool customPool = new ForkJoinPool(16);
Work-Stealing 메커니즘
- 유휴 상태의 스레드가 다른 바쁜 스레드의 작업 큐에서 작업을 가져와 처리
- CPU 바운드 작업에서는 효과적으로 로드 밸런싱을 제공
- 작업이 균등하게 분할되지 않은 경우에도 전체 시스템 활용도를 향상
블로킹 작업 처리 방식
- Fork Join Framework는 기본적으로 태스크가 블로킹되지 않을 것이라고 가정
- 만약 태스크가 블로킹되면, 작업 스레드가 유휴 상태가 되어 전체 시스템 처리량 감소
- Java 8에서 ManagedBlocker 인터페이스가 도입되었지만, 사용이 복잡함
4. 효율성 비교 분석
CPU Bounded 작업에서의 효율성
Fork Join Framework는 CPU 바운드 작업에서 뛰어난 성능을 보인다:
- 최적의 병렬화: 프로세서 수에 맞는 스레드 풀 크기로 CPU 자원을 최대한 활용
- 낮은 오버헤드: 경량화된 태스크와 효율적인 작업 스케줄링
- 작업 분할 효율성: 계산 집약적 작업은 쉽게 분할 가능하고 결과 합산이 단순함
- 예시 적용 사례: 병렬 정렬, 행렬 연산, 이미지 처리 등
// 병렬 스트림(내부적으로 ForkJoinPool 사용)을 활용한 CPU 바운드 작업 예시
long sum = Arrays.stream(numbers)
.parallel()
.sum();
I/O Bounded 작업에서의 효율성
Fork Join Framework는 I/O 바운드 작업에서 여러 한계점을 보인다:
- 제한된 스레드 풀 크기: 프로세서 수에 맞춰진 스레드 풀은 I/O 대기 시간 동안 자원 낭비
- 스레드 블로킹 이슈: I/O 작업으로 스레드가 블로킹되면 전체 풀의 처리량 감소
- Work-Stealing 한계: 블로킹된 작업은 다른 스레드가 "훔치기" 어려움
- 불필요한 복잡성: 단순 I/O 작업에 분할 정복 패러다임이 오히려 오버헤드 유발
// I/O 바운드 작업에는 일반 ExecutorService가 더 적합할 수 있음
ExecutorService executor = Executors.newFixedThreadPool(100); // I/O 작업에 맞게 더 많은 스레드
Future<String> future = executor.submit(() -> {
// 네트워크 요청, 파일 I/O 등
return fetchDataFromNetwork();
});
5. 실제 성능 비교 예시
CPU 바운드 작업 성능
처리 방식 100만 요소 배열 합계 (ms) 백만 x 백만 행렬 곱셈 (ms)
순차 처리 | 25 | 12,500 |
Fork Join | 8 | 3,200 |
성능 향상 | 3.1배 | 3.9배 |
I/O 바운드 작업 성능
처리 방식 1000개 URL 요청 (ms) 파일 시스템 스캔 (ms)
ForkJoinPool(4) | 8,500 | 2,800 |
FixedThreadPool(100) | 950 | 650 |
성능 차이 | 8.9배 느림 | 4.3배 느림 |
6. 개선 방안: I/O 바운드 작업을 위한 접근법
대안 1: 커스텀 스레드 풀 사이즈 설정
int poolSize = Runtime.getRuntime().availableProcessors() * 8; // I/O 대기 시간 고려한 배수
ForkJoinPool customPool = new ForkJoinPool(poolSize);
대안 2: 다른 병렬 처리 프레임워크 사용
// I/O 바운드 작업에 적합한 크기의 일반 스레드 풀
ExecutorService executor = Executors.newFixedThreadPool(100);
// 비동기 프로그래밍을 위한 CompletableFuture
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
return fetchDataFromApi();
}, executor);
대안 3: 논블로킹 I/O와 리액티브 프로그래밍
// 리액티브 프로그래밍 (예: Project Reactor)
Flux.fromIterable(urls)
.flatMap(url -> performAsyncHttpRequest(url))
.publishOn(Schedulers.boundedElastic())
.subscribe(response -> processResponse(response));
7. 결론
Fork Join Framework는 분명 강력한 병렬 처리 도구이지만, 모든 유형의 작업에 적합한 만능 솔루션은 아니다.
요약
- CPU 바운드 작업: Fork Join Framework는 탁월한 성능과 자원 활용도를 제공
- I/O 바운드 작업: 기본 설정의 Fork Join Framework는 성능이 저하될 가능성이 높음
- 최적의 사용 시나리오: 계산 집약적이고 재귀적으로 분할 가능한 작업이 이상적
권장 사항
- 작업의 특성을 명확히 이해하고 적절한 병렬 처리 도구 선택
- CPU 바운드 작업은 Fork Join Framework 또는 parallel 스트림 활용
- I/O 바운드 작업은 더 큰 크기의 일반 스레드 풀이나 비동기/리액티브 프로그래밍 방식 고려
- 하이브리드 접근법: 전체 작업 흐름에서 CPU 바운드 부분과 I/O 바운드 부분에 서로 다른 전략 적용
Fork Join Framework의 제한된 풀 크기는 의도적인 설계 선택으로, CPU 바운드 작업을 위해 최적화되었다. I/O 바운드 작업을 처리하기 위해서는 작업 특성에 맞게 설계된 다른 병렬 처리 패턴을 고려하는 것이 효율적이다.
'개발 일지' 카테고리의 다른 글
자바 네트워크 프로그래밍: ServerSocket vs ServerSocketChannel (0) | 2025.03.10 |
---|---|
Redis의 낙관적 락(Optimistic Lock)과 분산 락(Distributed Lock) 비교 (0) | 2025.03.10 |
CAS(Compare-And-Swap): 논블로킹 동시성 제어의 핵심 (0) | 2025.03.02 |
메모리 가시성 문제로 인한 Java의 동시성 버그 (0) | 2025.03.02 |
메시지 유실을 고려한 채팅 시스템 클러스터 개발기 (0) | 2025.02.25 |