Home [Java] Stream 병렬 처리
Post
Cancel

[Java] Stream 병렬 처리

자바에서의 동시성 처리

기존 자바에서는 병렬 처리를 위해 별도의 Thread를 만들어 관리하거나 ThreadPool을 사용했습니다. 주로 Executor를 사용해서 ThreadPool을 만들어 사용했는데, 이보다 더 쉽게 병렬처리할 수 있도록 Java8부터 Parallel Stream이 도입되었습니다.

사용법은 매우 단순합니다. 아래는 일반 stream을 사용하여 list의 요소들을 출력하는 예제입니다.

1
2
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
list.stream().forEach(n -> System.out.println(n));

이 코드는 순차적으로 실행됩니다. 그래서 순서대로 1~5가 출력됩니다. 여기서 stream을 parallel stream으로 바꿔주기만 하면 list를 병렬 처리할 수 있습니다.

1
2
3
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
list.parallelStream().forEach(n -> System.out.printf("%d - %s\n", 
    n, Thread.currentThread().getName()));

병렬처리되므로 요소가 실행할 때마다 무작위로 출력됩니다. 그리고 매번 다른 thread에서 처리됨을 볼 수 있습니다.

1
2
3
4
5
3 - Test worker
2 - ForkJoinPool.commonPool-worker-1
1 - ForkJoinPool.commonPool-worker-2
4 - Test worker
5 - ForkJoinPool.commonPool-worker-1

마법같은 일입니다. 이렇게 쉽게 병렬처리를 할 수 있다니.

하지만 이 parallel stream은 양날의 검입니다. 잘 알고 사용하지 않으면 오히려 성능이 떨어질 수 있습니다. 이 포스트에서는 병렬처리를 잘 이용할 수 있는 방법을 설명하겠습니다.


Fork-Join Framework

Parallel stream은 fork-join framework를 사용해서 병렬 처리를 구현합니다. 이건 새로운 기술이 아니라 java 7에도 있었던 다중 thread 작업 처리를 위한 기술입니다.

fork join framework는 작업을 분할하고(fork), 분할된 작업을 여러 thread에서 처리한 뒤에, 다시 합치는(join) 방식으로 동작합니다.

이때 사용하는 thread의 개수는 컴퓨터 환경의 cpu core수와 동일합니다. 정확히는 Runtime.getRuntime().availableProcessors()으로 알아낼 수 있습니다. 그래서 만약 4 core를 사용중이라면, 4개 작업까지는 4초가 걸리는 작업이 5개부터는 8초가 걸립니다.

사용하는 thread 수는 변경할 수 있습니다. JVM Parameter로 아래와 같이 설정해주면 됩니다.

1
2
3
4
5
// JVM Parameter - thread 8개 사용
-D java.util.concurrent.ForkJoinPool.common.parallelism=8

// 또는 코드로 변경 가능
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "8")

Work stealing

간단하게 표현하면, 바쁜 thread에게서 작업을 가져와 덜 바쁜 thread가 대신 처리하는 것입니다.

Fork-Join-Framework-Work-Stealing

하나의 worker는 동시에 1개 작업만 처리할 수 있습니다. 각자 worker는 별도의 처리해야할 작업 큐(deque로 구현)를 가지고 있습니다. 그리고 이 작업 큐에서 작업을 꺼내와 1개씩 처리합니다.

이때 작업이 없는 worker는 다른 worker로부터 작업을 가져와서 대신 처리합니다. 이러한 work stealing 덕분에 cpu 활용도를 극적으로 끌어올릴 수 있습니다.


사용시 주의할 점

공유되는 Fork-Join Pool

특별한 설정이 없다면, 실행되고 있는 프로그램 전체(동일한 JVM이라면 같은)에서 Fork-Join pool을 공유해서 사용합니다. 이 말은 모든 parallel stream이 같은 하나의 fork join pool을 사용한다는 말입니다.

그럼 여기서 무거운 IO작업 같은걸 돌리면 어떻게 될까요? 사용하고 있던 1개의 thread가 blocking되면서, 다른 parallel stream 작업이 처리되지 못하고 계속 대기 상태가 될 것입니다.

그래서 가능한 가벼운 작업만을 사용하거나, 직접 ForkJoinPool을 생성해서 사용해야합니다. 직접 생성해서 사용하는건 아래와 같이 사용하면 됩니다.

1
2
3
4
5
6
7
8
// 직접 pool을 만들어 submit
ForkJoinPool forkJoinPool = new ForkJoinPool(5);
forkJoinPool.submit(() -> {
    return list.stream()
        .parallel()
        .map(...process)
        .collect(Collectors.toList());
}).get();


작은 규모의 컬렉션

크기가 작은 collection은 순차 처리가 더 빠를 수도 있습니다. 병렬 처리를 위해서는 pool 생성, thread 생성, fork / join 등 여러 overhead가 있기 때문입니다.

크기가 작은 collection이라면 순차 처리를 고려해보세요.


코어의 수

코어의 수가 많을 수록 유리합니다. 하지만 싱글 코어인 경우에는 사용하지 않는 것이 좋습니다. 병렬 처리시 위에서 언급한 여러 overhead와, 병렬 처리를 위한 작업 scheduling이 있어서 싱글 코어인 경우에는 부가적으로 시간이 더 소요됩니다.

그래서 싱글 코어 환경에서는 오히려 순차 처리를 하는 편이 더 빠릅니다.


독립적인 작업

처리하고자 하는 작업들이 독립적이어야 더 빠릅니다. 만약 작업들이 서로 처리 순서가 중요하다면, 선순위 작업이 끝날 때 까지 대기하는 시간이 있으므로 병렬처리가 오히려 더 느려집니다. 작업들이 서로 종속적인 경우에는 순차처리를 해야합니다.


작은 작업

작업의 단위가 작으면 처리해야할 작업이 많아서 오히려 느려질 것 같습니다. 하지만 fork-join framework에는 work stealing이 있습니다. 그래서 cpu의 활용성을 최대한으로 높혀 처리하기 위해서는 작업이 작아야 유리합니다.


병렬 처리에 유리한 컬렉션

요소를 분리하기 쉬워야 한다.

분리하기 쉬운 기준은 크기를 알 수 있어야 합니다. 크기를 알아야 얼마나 작업을 분담할지 쉽게 파악할 수 있습니다.

메모리에 순차적으로 적재되는 구조여야 한다.

컴퓨터에는 메모리 캐시가 존재합니다. 메모리 캐시는 사용되는 메모리 영역 주변(locality)을 미리 캐싱합니다. 그래서 메모리에 순차적으로 데이터가 저장되어있으면, memory locality를 더 적극적으로 활용할 수 있습니다.

Array, ArrayList, HashMap등…

  • 쉽게 요소들을 쪼갤 수 있습니다. 그래서 병렬처리시 속도가 빠릅니다.
  • 특히 Array, ArrayList는 memory locality까지 활용할 수 있습니다.

LinkedList, TreeMap, HashSet등…

  • 요소를 쉽게 나눌 수 없습니다. 그래서 병렬 처리시 느립니다.
  • 심지어 데이터가 분리되어 저장되므로 memory locality를 활용할 수 없습니다.


Grouping 연산

stream을 reduce하는 연산은 빠르게 할 수 있습니다. 하지만 set이나 map에 collect하는 연산은 느립니다.

1
2
3
4
5
// 비교적 빠름
list.stream().parallel().reduce(0, Integer::sum);

// 다소 느림
list.stream().parallel().collect(Collectors.toSet());

참고

  1. 조슈아 블로크, 『Effective Java 3/E』, 프로그래밍 인사이트(2018)
  2. https://www.baeldung.com/java-when-to-use-parallel-stream
  3. https://gee.cs.oswego.edu/dl/html/StreamParallelGuidance.html
This post is licensed under CC BY 4.0 by the author.