7.2 포크/조인 프레임워크
포크/조인 프레임워크는 병렬화할 수 있는 작업을 재귀적으로 작은 작업으로 분할한 다음에 서브태스크 각각의 결과를 합쳐서 전체 결과를 만들도록 설계되었다.
- ExecutorService 인터페이스를 구현한다. (서브태스크를 스레드 풀의 작업자 스레드에 분산 할당하는)
7.2.1) RecursiveTask 활용
스레드 풀을 이용하려면 RecursiveTask<R> 의 서브클래스를 만들어야 한다. 이 클래스는 추상메서드 compute를 가진다.
if (태스크가 충분히 작거나 더 이상 분할할 수 없으면) {
순차적으로 태스크 계산
} else {
태스크를 두 서브태스크로 분할
태스크가 다시 서브태스크로 분할되도록 이 메서드를 재귀적으로 호출함
모든 서브태스크의 연산이 완료될 때까지 기다림
각 서브태스크 결과를 합침
}
포크/조인 프레임워크를 이용해서 병렬 합계를 수행할 수 있다.
public class ForkJoinSumCalculator extends RecursiveTask<Long> {
private final long[] numbers;
private final int start;
private final int end;
private static final long THRESHOLD = 10_000;
public ForkJoinSumCalculator(long[] numbers) {
this(numbers, 0, numbers.length);
}
public ForkJoinSumCalculator(long[] numbers, int start, int end) {
this.numbers = numbers;
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
int length = end - start;
if (length < THRESHOLD) {
return computeSequentially();
}
ForkJoinSumCalculator leftTask = new ForkJoinSumCalculator(numbers, start, start + length/2);
leftTask.compute();
ForkJoinSumCalculator rightTask = new ForkJoinSumCalculator(numbers, start + length/2, end);
Long rightResult = rightTask.compute();
Long leftResult = leftTask.join();
return leftResult + rightResult;
}
private Long computeSequentially() {
long sum = 0;
for (int i = start; i < end; i++) {
sum += numbers[i];
}
return sum;
}
}
7.2.2) 포크/조인 프레임워크를 제대로 사용하는 방법
- join 메서드를 태스크에 호출하면 태스크가 생산하는 결과가 준비될 때까지 호출자를 블록시킨다. 따라서 두 서브태스크가 모두 시작된 다음에 join을 호출해야 한다.
- RecursiveTask 내에서는 ForkJoinPool의 invoke 메서드를 사용하지 말아야 한다. 대신 compute나 fork 메서드를 직접 호출할 수 있다. 순차코드에서 병렬 계산을 시작할 때만 invoke를 사용한다.
- 서브태스크에 fork 메서드를 호출해서 ForkJoinPool의 일정을 조절할 수 있다.
- 포크/조인 프레임워크를 이용하는 병렬 계산은 디버깅 하기 어렵다.
- 병렬 스트림에서 살펴본 것처럼 멀티코어에 포크/조인 프레임워크를 사용하는 것이 순차 처리보다 무조건 빠를 거라는 생각은 버려야 한다.
7.2.3) 작업 훔치기
앞선 예제에서는 덧셈을 수행할 숫자가 만 개 이하면 서브태스크 분할을 중단했다. 기준값을 바꿔가면서 실험해보는 방법 외에는 좋은 기준을 찾을 뾰족한 방법이 없다. 하지만 실제로는 코어 개수와 관계없이 적절한 크기로 많은 태스크를 포킹하는 것이 바람직하다.
포크/조인 프레임워크에서는 작업 훔치기(Work Stealing)라는 기법으로 이 문제를 해결한다. 작업 훔치기 기법에서는 ForkJoinPool의 모든 스레드를 거의 공정하게 분할한다.
이 그림을 보면 이해가 쉬운데, 각각의 스레드는 자신에게 할당된 태스크를 포함하는 이중 연결 리스트를 참조하면서 작업이 끝날 때 마다 큐의 헤드에서 다른 태스크를 가져와서 작업을 처리한다. 모든 태스크가 작업을 끝낼 때 까지, 즉 모든 큐가 빌 때까지 이 과정을 반복한다. 따라서 태스크의 크기를 작게 나누어야 작업자 스레드 간의 작업부하를 비슷한 수준으로 유지할 수 있다.
이번 절에서는 숫자 배열을 여러 태스크로 분할하는 로직을 개발하며 예제를 살펴봤다. 그러나 분할 로직을 개발하지 않고도 병렬 스트림을 이용할 수 있었다. 즉, 스트림을 자동으로 분할해주는 기능이 있다는 사실을 이미 확인했다. 다음 절에서는 자동으로 스트림을 분할하는 기법인 Spliterator를 설명한다.
7.3 Spliterator 인터페이스
(내용이 익숙하지 않아서 생략, 필요한 경우 다시 찾아보는게 나을듯)
7.4 마치며
- 내부 반복을 이용하면 명시적으로 다른 스레드를 사용하지 않고도 스트림을 병렬로 처리할 수 있다.
- 간단하게 스트림을 병렬로 처리할 수 있지만 항상 병렬 처리가 빠른 것은 아니다. 병렬 소프트웨어 동작 방법과 성능은 직관적이지 않을 때가 많으므로 병렬 처리를 사용했을 때 성능을 직접 측정해봐야 한다.
- 병렬 스트림으로 데이터 집합을 병렬 실행할 때 특히 처리해야 할 데이터가 아주 많거나 각 요소를 처리하는 데 오랜 시간이 걸릴 때 성능을 높일 수 있다.
- 가능하면 기본형 특화 스트림을 사용하는 등 올바른 자료구조 선택이 어떤 연산을 병렬로 처리하는 것보다 성능적으로 더 큰 영향을 미칠 수 있다.
- 포크/조인 프레임워크에서는 병렬화할 수 있는 태스크를 작은 태스크로 분할한 다음에 분할된 태스크를 각각의 스레드로 실행하며 서브태스크 각각의 결과를 합쳐서 최종 결과를 생산한다.
- Spliterator는 탐색하려는 데이터를 포함하는 스트림을 어떻게 병렬화할 것인지 정의한다.
'Book > 모던 자바 인 액션' 카테고리의 다른 글
[모던 자바 인 액션] Chapter 08. 컬렉션 API 개선 (2) (1) | 2024.11.09 |
---|---|
[모던 자바 인 액션] Chapter 08. 컬렉션 API 개선 (1) (0) | 2024.11.07 |
[모던 자바 인 액션] Chapter 07. 병렬 데이터 처리와 성능 (1) (0) | 2024.10.29 |
[모던 자바 인 액션] Chapter 06. 스트림으로 데이터 수집 (2) (1) | 2024.10.25 |
[모던 자바 인 액션] Chapter 06. 스트림으로 데이터 수집 (1) (0) | 2024.10.25 |