티스토리 뷰

728x90
반응형

스트림 병렬화는 주의해서 적용하라


  • 주류 언어 중 자바는 동시성 프로그래밍 측면에서 항상 앞서왔습니다. 처음 릴리즈된 1996년부터 스레드, 동기화, wait/notify를 지원했습니다.
  • 자바로 동시성 프로그램을 작성하기는 쉬워지지만 올바르고 빠르게 작성하는 일은 여전히 어려운 작업입니다. 동시성 프로그래밍을 할때는 안전성응답 가능 상태를 유지하기 위해 노력해야하며, 병렬 스트림 파이프라인 프로그래밍에서도 다를바 없습니다.

 

🧨 Stream API의 병렬화에 문제가 있는 경우

  • 아래 예제는 메르센 소수를 20개 생성하는 코드인데 여기서 parallel() 메서드를 호출해서 여러개의 스레드를 활용해 동시성 프로그래밍으로 효율을 높힐 수 있다고 추측할 수 있지만 실제로 1시간이 넘어도 출력하지 못하는 응답 불가 상태에 빠지게 됩니다.
  • 그 이유는
    • 데이터 소스가 Stream,iterate이거나 중간 연산(limit)을 사용하는 경우 파이프라인 병렬화로 성능 향상을 시킬 수 없습니다.
    • 이유는 스트림 라이브러리에서 병렬화 시키는 방법을 찾지 못하기 때문인데 primes() 메서드에서 반환하는 스트림 파이프라인은 무한 스트림이고, limit로 제한을 두니 적절하게 여러 스레드에게 값을 분배할 수 없고, 지연 평가가 되기 때문에 각각의 값들의 참조 지역성도 떨어지게 됩니다.
public class Example {

    public static void main(String[] args) {
        primes().parallel()
                .map(p -> BigInteger.TWO.pow(p.intValueExact()).subtract(BigInteger.ONE))
                .filter(mersenne -> mersenne.isProbablePrime(50))
                .limit(20)
                .forEach(System.out::println);
    }

    public static Stream<BigInteger> primes() {
        return Stream.iterate(BigInteger.TWO, BigInteger::nextProbablePrime);
    }
}

 

💡 병렬화의 효과가 가장 좋을 때는?

  • 스트림의 소스가 ArrayList, HashMap, HashSet, ConcurrentHashMap의 인스턴스이거나 배열의 int 범위, long 범위일 때 병렬화의 효과가 가장 좋습니다.
  • 이 자료구조들은 모두 데이터를 원하는 크기로 절확하고 손쉽게 나눌 수 있어서 일을 다수의 스레드에 분배하기에 좋다는 특징이 있습니다.
  • 나누는 작업은 Spliterator가 담당하며, Spliterator 객체는 Stream이나 Iterable의 spliterator 메서드에서 얻을 수 있습니다.
  • 또한 이 자료구조들의 또 다른 중요한 공통점은 원소들을 순차적으로 실행할 때의 참조 지역성이 뛰어나다는 것입니다. 참조 지역성이란 이웃한 원소의 참조들이 메모리에 연속해서 저장되어 있다는 의미입니다.

 

💡 참조 지역성이 낮으면?

  • 참조들이 가리키는 실제 객체가 메모리에서 서로 떨어져 있을 수 있는데, 그러면 참조 지역성이 낮아집니다. 참조 지역성이 낮으면 스레드는 데이터가 주 메모리에서 캐시 메모리로 전송되어 오기를 기다리며 대부분의 시간을 멍하니 낭비하게 됩니다. 따라서 참조 지역성은 다량의 데이터를 처리하는 벌크 연산을 병렬화할 때 아주 중요한 요소로 작용됩니다. 
  • 참조 지역성이 가장 뛰어난 자료구조는 기본 타입의 배열입니다. 기본 타입 배열에서는(참조가 아닌) 데이터 자체가 메모리에 연속해서 저장되어 있기 때문입니다.

 

💡 스트림 파이프라인의 종단연산과 병렬화의 관계

  • 스트림 파이프라인의 종단 연산의 동작 방식 역시 병렬 수행 효율에 영향을 미칩니다. 종단 연산에서 수행하는 작업략이 파이프라인 전체 작업에서 상당 비중을 차지하면서 순차적인 연산이라면 파이프라인 병렬 수행의 효과는 제한될 수 있습니다. 
  • 종단 연산 중 병렬화에 가장 적합한 것은 축소입니다. 축소는 파이프라인에서 만들어진 모든 원소를 하나로 합치는 작업으로 Stream의 reduce 메서드 중 하나 또는 min, max, count, sum과 같이 완성된 형태로 제공되는 메서드 중 하나를 선택해 수행합니다.
  • anyMatch, allMatch, noneMatch처럼 조건에 맞으면 바로 반환되는 메서드도 병렬화에 적합합니다.
  • 반변 가변 축소를 수행하는 Stream의 collect 메서드는 병렬화에 적합하지 않습니다. 이유는 컬렉션을 합치는 부담이 크기 때문입니다.

https://catsbi.oopy.io/ 참고

 

💡 병렬화에 대해 잘 모르면 쓰지 말자

  • 스트림을 잘못 병렬화하면 성능이 나빠질 뿐만 아니라 결과 자체가 잘못되거나 예상치 못한 동작이 발생할 수 있습니다.
  • 결과가 잘못되거나 오동작하는것을 안전 실패라 하는데, 안전 실패는 병렬화한 파이프라인이 사용하는 mappers, filters 혹은 프로그래머가 제공한 다른 함수 객체가 명시한대로 동작하지 않을 때 발생할 수 있습니다.

 

💡 Stream 명세는 함수 객체에 대한 규약

  • Stream의 reduce 연산에 건네지는 accumulator(누적기)와 combiner(결합기) 함수는 반드시 결합법칙을 만족해야 합니다.
  • 간섭을 받지 않아야 합니다.- 파이프라인이 수행되는 동안 데이터 소스가 변경되지 않아야합니다.
  • 상태를 갖지 않아야 합니다.(stateless)
  • 이러한 요구 사항을 지키지 못하더라도 순차적으로 실행하면 올바른 결과를 얻을 수 있습니다. 하지만 병렬로 수행하면 기대값이 나오지 않을 수도 있고, 실패할 수도 있으니 주의가 필요합니다.

 

 

참고 자료)

https://jaehun2841.github.io/2019/02/17/effective-java-item48/#%EC%8A%A4%ED%8A%B8%EB%A6%BC-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EB%B3%91%EB%A0%AC%ED%99%94%EA%B0%80-%ED%9A%A8%EA%B3%BC%EC%A0%81%EC%9D%B8-%EC%98%88

https://catsbi.oopy.io/5861b4b5-60fa-4a6e-aadf-5caeafe68b1e

 

 

 

 

728x90
반응형