병렬 스트림
stream() 대신 parallelStream()을 사용하면 병렬 스트림이 생성된다.
각각의 스레드에서 처리할 수 있도록 스트림 요소를 여러 청크로 분할한 스트림으로 서 멀티코어 프로세서가 각각의 청크를 처리하도록 할당할 수 있다.
- 순차 스트림을 병렬 스트림으로 변경
public logn parallelSum(Long n) {
return Stream.iterate(1L, i -> i + 1)
.limit(n)
.parallel() <- 병렬 스트림화
.reduce(0L, Long::sum);
}
스트림 성능 측정
- JMH (자바 마이크로벤치마크 하니스) 라이브러리 사용
- @Benchmark를 붙여서 측정
병렬 스트림 효과적으로 사용하기
- 확신이 서지 않으면 직접 측정하라. 순차 스트림에서 병렬 스트림으로 쉽게 바꿀 수 있다. 반드시 적절한 벤치마크로 직접 성능을 측정하자
- 박싱은 성능을 저하 시킬 수 있기 때문에. 되도록이면 기본형 특화 스트림을 사용하자.
- 순차 스트림보다 병렬 스트림에서 성능이 떨어지는 연산들을 피하라. limit나 findFirst처럼 요소의 순서에 의존하는 연산을 병렬 스트림에서 수행하려면 비싼 비용을 치러야 한다.
- 스트림에서 수행하는 전체 파이프라인 비용을 고려하라. 처리해야할 요소 수가 N, 하나의 요소를 처리하는 데 드는 비용이 Q라하면 전체 스트림 파이프라인 처리비용은 N*Q로 예상할 수 있다. Q가 높아진다는 것은 병렬 스트림으로 성능을 개선할 수 있는 가능성이 있음을 의미한다.
- 소량의 데이터에서는 병렬 스트림이 도움되지 않는다. 병렬화 과정에서 생기는 부가 비용을 상쇄할 수 있을 만큼의 이득을 얻지 못하기 때문이다.
- 스트림을 구성하는 자료구조가 적절한지 확인하라. 예로, ArrayList는 LinkedList보다 효율적으로 분할할 수 있다. 또한 range 팩토리 메서드로 만든 기본형 스트림도 쉽게 분해할 수 있다. 또한 Spliterator를 구현해서 분해 과정을 완벽하게 제어할 수 있다.
- 스트림의 중간 연산이 스트림을 어떻게 변경하는 지 확인이 필요하다. 스트림의 길이를 예측할 수 없으면 효과적으로 병렬처리를 할 수 있는지 알 수 없다
- 최종 연산의 병합 비용을 살펴보자
포크/조인 프레임워크
병렬화할 수 있는 작업을 재귀적으로 작은 작업으로 분할한 다음 서브태스크 각각의 결과를 합쳐서 전체 결과를 만들도록 설계되었다.
RecursivTask 활용
- 쓰레드 풀 이용을 위해선 RecursiveTask의 서브클래스 혹은 RecursiveAction의 서브클래스를 만들어야 한다. RecursiveTask의 R은 병렬화된 태스크가 생성하는 결과 형식이고 RecursiveAction은 결과 형식이 없을 경우에 사용한다.
- RecursiveTask를 이용하기 위해선 compute 메서드를 구현해야 한다. compute 메서드는 태스크를 서브태스크로 분할하는 로직과 더 이상 분할할 수 없을 때 개별 서브태스크의 결과를 생산할 알고리즘을 정의한다.
포크/조인 프레임워크를 제대로 사용하는 방법
- join 메서드를 태스크에 호출하면 태스크가 생산하는 결과가 준비될 때까지 호출자를 블록시킨다. join 메서드는 두 서브태스크가 모두 시작된 다음에 호출하자.
- RecursiveTask 내에서는 ForkJoinPool의 invoke 메서드를 사용하지 말아야 한다. 대신 compute나 fork 메서드를 호출하자
- 왼쪽 작업과 오른쪽 모두에 fork메서드를 사용하는 것대신, 한쪽 작업에 compute를 호출하자. 두 서브태스크의 한 태스크에는 같은 스레드를 재사용할 수 있으므로 풀에서 불필요한 태스크를 할당하는 오버헤드를 줄일 수 있다.
- 디버깅이 어렵다는 점을 고려하자.
- 각 서브태스크의 실행시간은 새로운 태스크를 포킹하는 데 드는 시간보다 길어야 한다.
작업 훔치기
포크/조인 프레임워크에서는 작업 훔치기(work stealing)라는 기법을 사용한다.
각각의 스레드는 자신에게 할당된 태스크를 포함하는 이중 연결 리스트(doubley linked list)를 참조하여 작업이 끝날 때마다 큐의 헤드에서 다른 태스크를 가져와서 작업을 처리한다.
이때 한 스레드는 다른 스레드보다 자신에게 할당된 태스크를 더 빨리 처리할 수 있는데, 할일이 없어진 스레드는 유휴 상태로 바뀌는 것이 아니라 다른 스레드의 큐의 꼬리에서 작업을 훔쳐온다. 모든 태스크가 작업을 끝낼 때 까지 이 과정을 반복한다. 따라서 태스크의 크기를 작게 나누어야 작업자 스레드 간의 작업부하를 비슷한 수준으로 유지할 수 있다.
Spliterator 인터페이스
병렬 작업을 위한 자동 스르팀 분할 기법 모든 자료 구조에 대한 디폴트 Spliterator를 구현하여 제공하고 있다.
- tryAdvance : 요소를 순차적으로 소비하면서 탐색할 요소가 남아 있으면 참을 리턴
- trySplit : 분할하여 두 번째 Spliterator를 생성
- estimateSize : 탐색할 수의 정보 제공
- characteristics : Spliter의 특성을 의미
trySplit의 결과가 Null이 될 때까지 분할 호출하는 과정을 반복
728x90
'스터디 책 정리 > 모던 자바 인 액션' 카테고리의 다른 글
스트림으로 데이터 수집 (0) | 2023.09.12 |
---|---|
스트림 활용 (0) | 2023.09.11 |
스트림 (0) | 2023.09.11 |