JAVA/모던 자바 인 액션

모던 자바 인 액션 14. 병렬처리(1) - 병렬 스트림 조건

kujaHn 2021. 8. 21. 16:04

병렬 처리는 자바 7이 등장하기 전까지는 복잡한 절차를 거쳐야 했지만, 자바 7에서 '포크/조인 프레임워크' 기능을 제공하면서 더 쉽게 병렬화를 수행할 수 있게 되었습니다.

 

병렬 스트림

병렬 스트림이란 각 스레드에서 처리할 수 있도록 스트림 요소를 여러 청크로 분할한 스트림입니다.

이렇게 분할을 하면 멀티코어 스레드가 각각의 스트림을 처리를 하는 방식이죠.

 

그럼 직접 병렬 스트림을 만들어 봅시다!

 

일단 순차적 스트림을 만들어 봅시다.

private static long sequentialSum(long n) {
    return LongStream.iterate(1L, i -> i + 1)
        .limit(n)
        .reduce(0L, Long::sum);
}

딱히 설명할 것이 없는 코드입니다. 무한 스트림에 'limit( )'를 준 합계 스트림입니다.

이번에는 병렬 스트림입니다!

private long parallelSum(long n) {
        return LongStream.iterate(1L, i -> i + 1)
            .limit(n)
            .parallel()     		// 병렬화
            .reduce(0L, Long::sum);	// 병렬 연산 후 합침
}

기본적인 스트림에서 'parallel( )' 메서드를 통해 병렬 스트림으로 변환했습니다.

그다음 청크화 된 스트림을 병렬 리듀싱 연산을 실행하고, 마지막에 최종 결과를 합치는 형식으로 진행이 될 것입니다.

그림1 병렬 스트림

 

적절한 병렬 처리를 위해서는 몇 가지 체크해야 할 내용들이 있습니다.

 

1. 청크 분할 기준을 확실히 해라.

병렬 연산의 시작은 스트림을 청크화(나누는)것입니다. 일단 나누려면 '전체 리스트와, 나누는 기준' 이 두 가지 준비물이 필요합니다.

그림 2 전체 리스트와 나누는 기준이 필요한 이유

전체 리스트가 준비되지 않으면, 청크 분할 기준을 찾지 못하고 결국 기존의 순차처리와 다를게 없어집니다.

심지어 같은 동작을 하는데 불필요한 오버헤드만 증가하기 때문에, 기존보다 더 안 좋아지게 되는 것이죠.

 

 

2. 병렬 처리 자체도 비용이 든다.

세상이 그렇듯 공짜는 없습니다. 병렬 처리가 무적의 도구 같아 보이지만, 병렬 처리 자체도 비용이 적지 않게 들어갑니다. 그렇기 때문에, 병렬 처리가 꼭 필요한 작업인지 판단을 해야 합니다.

(데이터가 적으면 병렬 처리비용이 데이터 연산 비용보다 더 많아지는 상황이 발생할 수 있습니다.)

 

이는 벤치마크 툴(JMH)을 이용해 어느 것이 시간이 더 적게 드는지를 테스트하면 의사결정에 도움이 됩니다.+

(Plus. 분할 연산 후 합칠 때의 비용도 꼭 고려해야 합니다!)

 

3. Stateful 한 변수를 다루어서는 안 된다. (공유된 상태를 바꾸는 알고리즘을 피해라!)

여러 스레드에서 공유가 되는 변수 즉 Stateful 한 변수는 치명적인 에러를 불러옵니다.

private static long sideEffectSum(long n) {
    Accumulator accumulator = new Accumulator();
    LongStream.rangeClosed(1, n).parallel().forEach(accumulator::add);
    return accumulator.total;
}

private static class Accumulator {
    long total = 00;
    void add(long value) {
        total += value;}
}

정말 간단한 코드입니다. total이 Stateful한 변수인 것을 주목합시다.

 

public static void main(String[] args) {
    for (int i = 0; i <5; i++) {
        System.out.println("병렬 테스트 확인 : "
            + sideEffectSum(1000)
        );
    }
}

그리고 이 병렬 스트림 연산을 수행하면....

병렬 테스트 확인 : 485259
병렬 테스트 확인 : 402039
병렬 테스트 확인 : 457969
병렬 테스트 확인 : 400046
병렬 테스트 확인 : 396922

실행 결과 모두 다 다르게 나옵니다!

이는 여러 스레드에서 total을 공유해, 계산할 total값들이 계속해서 달라지기 때문입니다. (= 아토믹 연산이 아니다.)

 

4. 순차 스트림에 더 유리한 연산 및 자료구조 타입이 있다.

연산

'limit( )'나, 'findFirst( )' 같이 요소의 순서가 중요시되는 연산은 병렬 스트림에서 굉장히 불리한 조건을 가집니다.

(연산 + 순서 판별까지 고려를 해야 합니다. 그러므로 'findFirst( )' 보다는 'findAny( )'가 더 유리하겠죠?)

 

자료구조

자료구조 역시 순서와 상관이 있다면 불리함을 가집니다. 가령 앞 뒷 노드를 알아야 하는 'LinkedList'의 경우 'ArrayList' 보다 더 불리합니다. (유리 : 'ArrayList' / 'range( )' / 'HashSet' / 'TreeSet'        불리 : 'LinkedList' / 'iterate( )')