일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- @EnableScheduling
- @configuration
- 다리를 지나는 트럭
- 알고리즘
- 영속 자료구조
- 코딩 테스트
- @Setter
- 고차원 함수
- 크론 표현식
- H-index
- 루씬 인 액션
- 롬복 어노테이션
- @Data
- 쿠버네티스
- @Getter
- 커링
- 가장 큰 수
- 스프링 스케쥴러
- 전화번호 목록
- 해시
- 스택/큐
- kubenetes
- 프로그래머스
- 검색 기능 확장
- K번째수
- 완주하지 못한 선수
- 모던 자바 인 액션
- Java
- 정렬
- 기능개발
- Today
- Total
Today I Learned
[모던 자바 인 액션] 7장. 병렬 데이터 처리와 성능 (2) 본문
7.2 포크/조인 프레임워크
병렬화할 수 있는 작업을 재귀적으로 작은 작업으로 분할하여 서브태스크로 처리한 뒤, 각각의 결과를 합쳐서 전체 결과로 만드는 방식
7.2.1 RecursiveTask 활용
스레드풀을 이용하려면 RecursiveTask<R>의 서브클래스를 만들어야하고, 추상메서드 compute를 구현해야 한다.
protected abstract R compute();
compute 메서드 구현 형식은 분할 정복 알고리즘의 병렬화 버전을 사용한다.
if(태스크가 충분히 작거나 더이상 분할할 수 없으면) {
순차적으로 태스크 계산
} else {
태스크를두 서브태스크로 분할
태스크가 다시 서브태스크로 분할되도록 메시지를 재귀적으로 호출
모든 서브태스크의 연산이 왑료될때까지 대기
각 서브태스크의 결과를 합침
}
7.2.2 포크/조인 프레임워크를 제대로 사용하는 방법
- join 메서드를 태스크에 호출하면 태스크가 생산하는 결과가 준비될 때까지 호출자를 블록시킨다. 따라서 두 서브태스크가 모두 시작된 다음에 join을 호출하지 않으면, 각각의 서브태스크가 다른 서브태스크를 기다리는 일이 발생할 수 있다.
- RecursiveTask 내에서는 compute나 fork 메서드를 사용하며, 순차코드에서 병렬 계산을 시작할때만 ForkJoinPool의 invoke 메서드를 사용해야 한다.
- 서브태스크에 fork 메서드를 호출해서 ForkJoinPool의 일정을 조절할 수 있다. 한쪽 작업에만 fork를 호출하고 다른쪽에는 compute를 호출하면, 한 태스크에는 같은 스레드를 재사용할 수 있으므로 불필요한 오버헤드를 피할 수 있다.
- 포크/조인 프레임워크의 병렬 계산은 디버깅하기 어렵다. fork라 불리는 스레드에서 compute를 호출하므로 스택 트레이스가 도움이 되지 않는다.
- 병렬 처리로 성능을 개선하려면 태스크를 여러 독립적인 서브태스크로 분할할 수 있어야 하며, 각 서브태스크의 실행 시간은 새로운 태스크를 포킹하느데 드는 시간보다 길어야한다.
7.2.3 작업 훔치기
이론적으로는 CPU의 코어 개수만큼 병렬화된 태스크로 작업부하를 분할하면 모든 코어에서 태스크를 실행할 것이고, 같은 시간에 종료될 것이라고 생각할 수 있다. 하지만 다양한 이유로 각각의 서브태스크의 작업완료 시간이 크게 달라질 수 있다.
포크/조인 프레임워크에서는 작업훔치기(work stealing)라는 기법으로 이 문제를 해결한다.
각각의 스레드는 자신에게 할당된 작업이 모두 끝나면 다른 스레드의 큐에서 작업을 가져와 처리한다. 따라서 태스크의 크기를 작게 나누어야 스레드 간의 작업부하를 비슷한 수준으로 유지할 수 있다. 실제 코어 수 보다 더 잘게 나누는 이유이다.
7.3 Spliterator 인터페이스
Spliterator는 '분할할 수 있는 반복자'라는 의미이다. Iterator처럼 소스의 요소 탐색 기능을 제공하며, 병렬 작업에 특화되어 있다.
public interface Spliterator<T> {
boolean tryAdvance(Consumer<? super T> action);
Spliterator<T> trySplit();
long estimateSize();
int characteristics();
}
tryAdvace - Spliterator의 요소를 순차적으로 소비하면서 탐색해야할 요소가 있으면 참을 반환
trySplit - Spliterator의 일부 요소(자신이 반환한 요소)를 분할해서 두 번째 Spliterator를 생성
estimateSize - 탐색해야할 요소 수
characteristics - Spliterator의 특성을 정의
7.3.1 분할 과정
스트림을 여러 스트림으로 분할하는 과정은 재귀적으로 일어난다.
1단계에서 첫 번째 Spliterator에서 trySplit을 호출하면 두 번째 Spliterator가 생성되고, 2단계에서 두 번째 Spliterator에서 trySplit을 호출하면 네 개의 Spliterator가 생성된다. 이는 trySplit가 null이 될때까지 반복한다.
Spliterator 특성
characteristics 추상 메서드로 정의하며, Spliterator 자체 특성 집항르 포함하는 int를 반환한다.
특성 | 의미 |
ORDERED | 리스트처럼 정해진 순서가 있으므로 요소를 탐색하고 분할할 때 순서에 유의해야 함 |
DISTINCT | x, y 두 요소를 방문했을 때 x.equals(y)는 항상 false를 반환 |
SORTED | 탐색된 요소는 미리 정의된 정렬 순서를 따른다. |
SIZED | 크기가 알려진 소스로 생성했으므로 estimatedSize()는 정확한 값을 반환한다. |
NON-NULL | 탐새갛느 모든 요소는 null이 아니다. |
IMMUTABLE | 이 Spliterator의 소스는 불변이다. 요소를 탐색하는 동안 추가/삭제/수정할 수 없다. |
CONCURRET | 동기화 없이 Spliterator의 소스를 여러 스레드에서 동시에 고칠 수 있다. |
SUBSIZED | 이 Spliterator와 분할되는 모든 spliterator의 SIZED 특성을 갖는다. |
7.3.2 커스텀 Spliterator 구현하기
반복형으로 단어 수를 세는 메서드
public int countWordsIteratively(String s) {
int counter = 0;
boolean lastSpace = true;
for (char c : s.toCharArray()) { //문자열의 모든 문자를 하나씩 탐색
if (Character.isWhiteSpace(c)) {
lastSpace = true;
} else {
if (lastSpace) conter++; //공백문자 탐색 시 이전까지의 문자를 단어를 간주하여 단어 수 counting
lastSpace = false;
}
}
return counter;
}
함수형으로 단어 수를 세는 메서드
class WordCounter {
private final int counter;
private final boolean lastSpace;
public WordCounter(int counter, boolean lastSpace) {
this.counter = counter;
this.lastSpace = lastSpace;
}
public WordCounter accumulate(Character c) {
if (Character.isWhitespace(c)) {
return lastSpace ? this : new WordCounter(counter, true);
} else {
return lastSpace ? new WordCounter(counter+1, false) : this;
}
}
public WordCounter combine(WordCounter wordCounter) {
return new WordCounter(counter + wordCounter.conter, wordCounter.lastSpace);
}
public int getConter() {
return conter;
}
}
스트림을 탐색하며 새로운 문자를 찾을때마다 accumulate를 호출한다.
accumulate 메서드는 새로운 WordCounter 클래스를 어떤 상태로 생성할 것인지 정의한다.
combine은 문자열 서브스트림을 처리한 WordCounter의 결과를 합친다.
위 코드를 문자열 스트림의 리듀싱 연산으로 구현해보면,
private int countWord(Stream<Character> stream) {
WordCounter wordCounter = stream.reduce(
new WordCounter(0, true),
WordCounter::accumulate,
WordCounter::combine);
return wordCounter.getCounter();
}
WordCounter 병렬로 수행하기
위 연산을 병렬 스트림으로 처리하면 원하는 결과가 나오지 않는다. 원래 문자열을 임의의 위치에서 둘로 나누다보니 하나의 단어를 둘로 계산하는 상황이 발생할 수 있기 때문이다.
따라서 문자열을 임의의 위치에서 분할하지 않고 단어가 끝나는 위치에서만 분할하도록 trySplit() 메서드를 구현해주면 된다.
'JAVA & Spring > 모던 자바 인 액션' 카테고리의 다른 글
[모던 자바 인 액션] 9장. 리팩터링, 테스팅, 디버깅 (0) | 2021.09.01 |
---|---|
[모던 자바 인 액션] 8장. 스트림과 람다를 이용한 효과적 프로그래밍 (0) | 2021.08.23 |
[모던 자바 인 액션] 7장. 병렬 데이터 처리와 성능 (1) (0) | 2021.08.04 |
[모던 자바 인 액션] 6장. 스트림으로 데이터 수집 (2) (0) | 2021.07.28 |
[모던 자바 인 액션] 6장. 스트림으로 데이터 수집 (1) (1) | 2021.07.22 |