[JAVA] Fork/Join Framework

JHJeong·2024년 4월 27일
0
post-custom-banner

JAVA7에서 추가된 Fork/Join Framework를 설명하기 전에 기존에 ThreadPool에 대해서 설명해보면 다음과 같다.

ThreadPool

ThreadPool은 여러 스레드들 미리 생성하고 관리하는 프로그래밍 구조이다. 이를 통해서 멀티 스레드 애플리케이션에서의 성능을 향상 시킬수 있는데, 스레드를 필요할 때마다 생성하고, 소멸시키는 비용은 상당히 높기 때문에, 미리 생성된 스레드들을 재사용함으로써 이러한 비용을 절감할 수 있다는 장점이 있다.

ThreadPool의 기본 작동 방식

  1. 스레드 생성 : 애플리케이션이 시작할 대 스레드 풀은 설정된 수만큼의 스레드를 생성한다.
  2. 작업 요청 처리 : 작업 요청이 들어오면, 스레드 풀은 이용가능한 스레드 중 하나를 할당하여 작업을 처리하도록한다.
  3. 스레드 재사용 : 작업을 완료한 스레드는 다시 스레드 풀로 반환되어 다음 작업을 대기한다/

ThreadPool의 장점

  • 리소스 효율성 : 스레드의 생성과 소멸에 필요한 자원과 시간을 절약할 수 있다.
  • 응답성 향상 : 작업을 즉시 처리할 수 있는 스레드가 이미 존재하기 때문에, 응답시간이 단축된다.
  • 스레드 관리 : 스레드의 최대 개수를 제어하여 시스템 자원의 과도한 사용을 방지할 수 있다.

ThreadPool의 단점

  • 오버헤드 : 스레드 풀을 관리하는데에는 약간의 오버헤드가 발생한다.
  • 자원 공유 문제 : 여러 스레드가 자원을 공유하므로, 동기화 문제가 발생할 수 있다.

JAVA에서는 'java.util.concurrent' 패키지 안에 'ExecutorService' 인터페이스와 'Excutors' 유틸리티 클래스를 통해 스레드 풀을 쉽게 사용할 수 있다.

Fork/Join Framework

Fork/Join Framwork는 JAVA 7에서 도입된 병렬 처리를 위한 프레임워크이고, 주로 큰 작업을 작은 작업으로 나누고 작업을 병렬로 처리한 후 그 결과를 합치는 분할 정복 방식에 최적화 되어 있다. 이 프레임워크는 'java.util.concurrent' 패키지의 일부로 특히 RecursiveAction과 RecursiveTask 두 가지 주요 추상 클래스를 제공한다.

작업 훔치기(Work-Stealing) 알고리즘을 사용하여, 각 스레드는 자신의 큐에 할당된 작업을 수행하다가 일을 마치면 다른 스레드의 큐에서 대기 중인 작업을 훔쳐와서 실행한다. 이 방식은 모든 스레드가 계속해서 작업을 처리할 수 있도록 하고 있기 때문에 리소스의 활용도를 높인다.

ThreadPool과 Fork/Join Framework 비교

- Fork/Join Framework

  1. 효율적인 작업 분배 : 작업 훔치기 알고리즘을 통해 유휴 상태의 스레드가 발생할 확률을 최소화하고, 모든 스레드가 균등하게 작업을 처리함
  2. 높은 병렬 처리 성능 : 분할 정복 알고리즘에 적합하며, 복잡한 계산 작업을 빠르게 처리할 수 있음
  3. 구현 복잡성 : 기본적인 스레드 풀 사용에 비해 작업 분할 및 결합 로직을 직접 구현해야하므로 복잡할 수 있음
  4. 특정 상황에 제한적 : 주로 분할 정복 방식에 적합하므로, 모든 종류의 작업에 이 프레임워크를 사용하는 것은 적절하지 않을 수 있음

- ThreadPool

  1. 간단한 사용법 : ExecutorService 인터페이스를 사용하여 스레드 풀을 쉽게 구현하고 관리할 수 있음
  2. 다양한 유형지원 : 고정 크기, 캐시된 스레드 풀, 단일 스레드 실행자 등 다양한 스레드 풀을 선택할 수 있음
  3. 리소스 낭비 가능성 : 고정된 수의 스레드를 사용하기 때문에, 일부 스레드가 유휴 상태가 되거나 반대로 과부하가 발생할 수 있음
  4. 동적 조정 미지원 : 스레드 수를 동적으로 조정하는 것이 어렵거나 불가능할 수 있어, 변동적인 작업 부하에 대응하기 어려움

Fork/Join Framework는 복잡한 계산이나 대량의 데이터 처리에 적합하고, 일반적인 ThreadPool은 간단하고 반복적인 작업에 더 적합할 수 있다. ThreadPool과 Fork/Join Framework를 사용하는 예제 소스를 보면서 비교해보자.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class ThreadPoolExample {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executor = Executors.newFixedThreadPool(4); // 4개의 스레드를 가진 풀 생성

        for (int i = 0; i < 20; i++) {
            int taskNumber = i;
            executor.execute(() -> {
                System.out.println("Executing Task " + taskNumber + " on thread " + Thread.currentThread().getName());
                try {
                    Thread.sleep(1000); // 시뮬레이션을 위한 1초 대기
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }

        executor.shutdown();
        executor.awaitTermination(1, TimeUnit.HOURS);
    }
}

출력 결과

Executing Task 3 on thread pool-1-thread-4
Executing Task 0 on thread pool-1-thread-1
Executing Task 2 on thread pool-1-thread-3
Executing Task 1 on thread pool-1-thread-2
Executing Task 5 on thread pool-1-thread-1
Executing Task 4 on thread pool-1-thread-3
Executing Task 6 on thread pool-1-thread-2
Executing Task 7 on thread pool-1-thread-4
Executing Task 8 on thread pool-1-thread-1
Executing Task 9 on thread pool-1-thread-3
Executing Task 10 on thread pool-1-thread-2
Executing Task 11 on thread pool-1-thread-4
Executing Task 12 on thread pool-1-thread-1
Executing Task 13 on thread pool-1-thread-3
Executing Task 14 on thread pool-1-thread-2
Executing Task 15 on thread pool-1-thread-4
Executing Task 16 on thread pool-1-thread-1
Executing Task 17 on thread pool-1-thread-3
Executing Task 18 on thread pool-1-thread-2
Executing Task 19 on thread pool-1-thread-4

기존 JAVA에서 ExecutorService를 사용하여 간단한 작업을 병렬로 처리하는 예제이다.
이 코드는 20개의 작업을 생성하고, 고정된 크기(4개)의 스레드 풀에서 이 작업들을 실행한다. 각 작업은 현재 실행중인 스레드의 이름을 출력하고, 1초간 대기한다.

ThreadPool의 한계점

  • 부하 균형 문제 : 위 예제에서 스레드 풀은 고정된 수의 스레드(4개)를 가진다. 모든 스레드가 동시에 작업을 처리하지만, 특정 시점에서 작업의 수가 스레드 수보다 적거나 많을 수 있다. 이 경우 일부 스레드 유휴 상태가 되거나, 작업이 균등하게 분배되지 않아 효율성이 떨어질 수 있다.
  • 동적 조정 미지원 : 고정된 수의 스레드를 사용하므로, 시스템의 부하에 따라 스레드 수를 동적으로 조정할 수 없다. 이는 리소스 활용도와 성능에 제한을 준다.
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;

public class ForkJoinExample extends RecursiveAction {
    private final int start;
    private final int end;
    private static final int THRESHOLD = 5;

    public ForkJoinExample(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected void compute() {
        if (end - start <= THRESHOLD) {
            for (int i = start; i < end; i++) {
                System.out.println("Processing " + i + " on thread " + Thread.currentThread().getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        } else {
            int mid = start + (end - start) / 2;
            ForkJoinExample left = new ForkJoinExample(start, mid);
            ForkJoinExample right = new ForkJoinExample(mid, end);
            invokeAll(left, right);
        }
    }

    public static void main(String[] args) {
        ForkJoinPool pool = new ForkJoinPool();
        pool.invoke(new ForkJoinExample(0, 20));
        pool.shutdown();
    }
}

출력 결과

Processing 15 on thread ForkJoinPool-1-worker-4
Processing 5 on thread ForkJoinPool-1-worker-3
Processing 10 on thread ForkJoinPool-1-worker-2
Processing 0 on thread ForkJoinPool-1-worker-1
Processing 16 on thread ForkJoinPool-1-worker-4
Processing 6 on thread ForkJoinPool-1-worker-3
Processing 11 on thread ForkJoinPool-1-worker-2
Processing 1 on thread ForkJoinPool-1-worker-1
Processing 17 on thread ForkJoinPool-1-worker-4
Processing 12 on thread ForkJoinPool-1-worker-2
Processing 7 on thread ForkJoinPool-1-worker-3
Processing 2 on thread ForkJoinPool-1-worker-1
Processing 13 on thread ForkJoinPool-1-worker-2
Processing 8 on thread ForkJoinPool-1-worker-3
Processing 3 on thread ForkJoinPool-1-worker-1
Processing 18 on thread ForkJoinPool-1-worker-4
Processing 14 on thread ForkJoinPool-1-worker-2
Processing 9 on thread ForkJoinPool-1-worker-3
Processing 19 on thread ForkJoinPool-1-worker-4
Processing 4 on thread ForkJoinPool-1-worker-1

이 코드는 비슷한 작업을 분할 정복 방식으로 처리하는 예제이다. 작업을 더 작은 단위로 나누고 각 부분을 별도의 스레드에서 처리한다. invokeAll 메소드는 분할된 작업들을 병렬로 실행하고 완료될 때까지 기다린다. 이 방식은 작업이 균등하게 분배되고, 스레드 풀의 스레드가 최대한 효율적으로 사용된다.

profile
이것저것하고 싶은 개발자
post-custom-banner

0개의 댓글