Application/JAVA & Kotlin

[모던 자바 인 액션 정리] 7장. 병렬 데이터 처리와 성능

반응형

자바 7 이전 병렬 처리


데이터를 서브 파트로 분할하고, 서브 파트를 각각의 스레드로 할당한다. 스레드로 할당한 다음에는 의도치 않은 레이스 컨디션이 발생하지 않도록 적절한 동기화를 추가해야 하며 마지막으로 부분 결과를 합쳐야 한다.

 

자바 7의 새로운 병렬 처리 방법


포크/조인 프레임워크 기능을 제공

 

자바 8의 새로운 병렬 처리 방법


스트림을 통한 병렬 처리

 

병렬 스트림


  • 스트림에 parallelStream()을 호출하면 병렬 스트림이 생성된다.
  • 병렬 스트림이란 각각의 스레드에서 처리할 수 있도록 스트림 요소를 여러 청크로 분할한 스트림.
  • 병렬 스트림을 이용하면 모든 멀티코어 프로세서가 각각의 청크를 처리할 수 있도록 할당할 수 있다.
Stream.iterate(1L, i -> i + 1)
			.limit(n)
			.parallel() // 스트림을 병렬 스트림으로 변환
			.reduce(0L, Long::sum);

 

병렬 스트림 주의 사항


  • 병렬화가 완전 공짜는 아니다
  • 병렬화를 이용하려면 스트림을 재귀적으로 분할해야 하고, 각 서브 스트림을 서로 다른 스레드의 리듀싱 연산으로 할당하고, 이들 결과를 하나의 값으로 합치는 오버헤드가 존재.
  • 병렬 스트림과 병렬 계산에서는 공유된 가변 상태를 피해야 한다.
public long sideEffectSum(long n) {
		Accumulator accumulator = new Accumulator();
		LongStream.rangeClosed(1, n)
							.parallel()
							.forEach(accumulator::add);
		return accumulator.total;
}

public class Accumulator {
		public long total = 0;
		public void add(long value) {
				total += value; // 여러 스레드에서 공유하는 객체의 상태를 바꿈
		}
}

 

병렬 스트림 효과적으로 사용하기


  1. 확신이 서지 않으면 직접 측정하라.
    • 무조건 병렬 스트림으로 바꾸는 것이 능사는 아니다. (항상 병렬 스트림이 순차 스트림보다 빠른 것은 아님)
    • 적절한 벤치마크로 직접 성능을 측정하는 것이 바람직함.
  2. 박싱을 주의하라.
    • 자동 박싱과 자동 언박싱은 성능을 크게 저하시킬 수 있는 요소이다.
    • 기본형 특화스트림(IntStream, LongStream, DoubleStream)을 사용.
  3. 순차 스트림보다 병렬 스트림에서 성능이 떨어지는 연산이 있다.
    • 특히나 limit나 findFirst처럼 요소의 순서에 의존하는 연산을 병렬 스트림에서 수행하려면 비싼 비용을 치러야 한다.
  4. 스트림에서 수행하는 전체 파이프라인 연산 비용을 고려하라.
    • 처리해야 할 요소 수가 N이고, 하나의 요소를 처리하는 데 드는 비용이 Q라 하면 전체 스트림 파이프라인 처리 비용을 N*Q로 예상할 수 있다. Q가 높아진다는 것은 병렬 스트림으로 성능을 개선할 수 있는 가능성이 있음을 의미
  5. 소량의 데이터에서는 병렬 스트림이 도움이 되지 않는다.
    • 1번과 마찬가지로, 병렬화 과정에서 생기는 부가 비용을 상쇄할 수 있는 만큼의 이득을 얻지 못하기 때문.
  6. 스트림을 구성하는 자료구조가 적절한지 확인.
    • 예를 들어 ArrayList를 LinkedList보다 효율적으로 분할할 수 있다.
  7. 스트림의 특성과 파이프라인의 중간 연산이 스트림의 특성을 어떻게 바꾸는지에 따라 분해 과정의 성능이 달라질 수 있다.
    • 예를 들어 SIZED 스트림은 정확히 같은 크기의 두 스트림으로 분할할 수 있으므로 효과적으로 스트림을 병렬 처리할 수 있다. 반면 필터 연산이 있으면 스트림의 길이를 예측할 수 없으므로 효과적으로 스트림을 병렬 처리할 수 있을지 알 수 없게 된다.
  8. 최종 연산의 병합 과정 비용을 살펴보라.
    • 병합 과정이 비용이 비싸다면 병렬 스트림으로 얻은 성능의 이익이 서브 스트림 부분 결과를 합치는 과정에서 상쇄될 수 있다.

 

포크/조인 프레임워크


병렬화할 수 있는 작업을 재귀적으로 작은 작업으로 분할한 다음에 서브 태스크 각각의 결과를 합쳐서 전체 결과를 만들도록 설계됨.


Spliterator 인터페이스


  • Spliterator은 탐색하려는 데이터를 포함하는 스트림을 어떻게 병렬화할 것인지에 대한 정의.
  • 병렬 스트림에서는 분할 로직을 개발하지 않고도 이용할 수 있었음. 즉 스트림을 자동으로 분할해주는 기능 ⇒ Spliterator
public interface Spliterator<T> {
		boolean tryAdvance(Consumer<? super T> action);
		Spliterator<T> trySplit();
		long estimateSize();
		int characteristics();
}
  • T는 Spliterator에서 탐색하는 요소의 형식을 가리킨다.
  • tryAdvance 메소드는 Spliterator의 요소를 하나씩 순차적으로 소비하면서 탐색해야 할 요소가 남아있으면 참을 반환한다.
  • trySplit 메소드는 Spliterator의 일부 요소를 분할해서 두 번째 Spliterator을 생성하는 메소드.
  • estimateSize()로 탐색해야 할 요소 수 정보를 제공할 수 있다.


분할 과정

  • 스트림을 여러 스트림으로 분할하는 과정은 재귀적으로 일어난다.
  • 첫 번째 Spliterator에서 trySplit()을 호출하면 두 번째 Spliterator가 생성됨.
  • 2단계에서 두 개의 Spliterator에 trySplit을 호출하면 네 개의 Spliterator가 생성됨.
  • 이처럼 trySplit의 결과가 null이 될 때까지 이 과정을 반복. (null을 반환한다는 것은 더 이상 자료구조를 분할할 수 없음을 의미)

반응형