Notice
Recent Posts
Recent Comments
Link
«   2025/03   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31
Archives
Today
Total
관리 메뉴

csct3434

Fork Join Framework: CPU bounded vs I/O bounded 작업 효율성 비교 본문

개발 일지

Fork Join Framework: CPU bounded vs I/O bounded 작업 효율성 비교

csct3434 2025. 3. 10. 08:42

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 바운드 작업에서 뛰어난 성능을 보인다:

  1. 최적의 병렬화: 프로세서 수에 맞는 스레드 풀 크기로 CPU 자원을 최대한 활용
  2. 낮은 오버헤드: 경량화된 태스크와 효율적인 작업 스케줄링
  3. 작업 분할 효율성: 계산 집약적 작업은 쉽게 분할 가능하고 결과 합산이 단순함
  4. 예시 적용 사례: 병렬 정렬, 행렬 연산, 이미지 처리 등
// 병렬 스트림(내부적으로 ForkJoinPool 사용)을 활용한 CPU 바운드 작업 예시
long sum = Arrays.stream(numbers)
               .parallel()
               .sum();

I/O Bounded 작업에서의 효율성

Fork Join Framework는 I/O 바운드 작업에서 여러 한계점을 보인다:

  1. 제한된 스레드 풀 크기: 프로세서 수에 맞춰진 스레드 풀은 I/O 대기 시간 동안 자원 낭비
  2. 스레드 블로킹 이슈: I/O 작업으로 스레드가 블로킹되면 전체 풀의 처리량 감소
  3. Work-Stealing 한계: 블로킹된 작업은 다른 스레드가 "훔치기" 어려움
  4. 불필요한 복잡성: 단순 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는 성능이 저하될 가능성이 높음
  • 최적의 사용 시나리오: 계산 집약적이고 재귀적으로 분할 가능한 작업이 이상적

권장 사항

  1. 작업의 특성을 명확히 이해하고 적절한 병렬 처리 도구 선택
  2. CPU 바운드 작업은 Fork Join Framework 또는 parallel 스트림 활용
  3. I/O 바운드 작업은 더 큰 크기의 일반 스레드 풀이나 비동기/리액티브 프로그래밍 방식 고려
  4. 하이브리드 접근법: 전체 작업 흐름에서 CPU 바운드 부분과 I/O 바운드 부분에 서로 다른 전략 적용

Fork Join Framework의 제한된 풀 크기는 의도적인 설계 선택으로, CPU 바운드 작업을 위해 최적화되었다. I/O 바운드 작업을 처리하기 위해서는 작업 특성에 맞게 설계된 다른 병렬 처리 패턴을 고려하는 것이 효율적이다.