상세 컨텐츠

본문 제목

병렬 처리 (동시성, 병렬성), 병렬 스트림, 처리성능 요소

기록 - 프로그래밍/Java

by wjjun 2024. 2. 27. 13:00

본문

 

병렬처리

멀티 코어 CPU 환경에서 작업을 분할합니다. 병렬적으로 코어들 마다 작업을 처리하며 작업 처리 시간을 줄이기 위해 사용됩니다.

 

동시성과 병렬성

멀티 스레드는 동시성이나 병렬성으로 실행되기 때문에 용어를 정확히 아는 것도 중요합니다.

둘다 멀티 스레드 동작 방식인 점은 같지만 목적이 서로 다릅니다.

 

1. 동시성은 멀티 작업을 위해 멀티 스레드가 번갈아가면서 실행되는 성질입니다.

2. 병렬성은 멀티 작업을 위해 멀티 코어를 이용해 동시에 실행하는 것을 의미합니다.

 

싱글 코어 CPU를 이용한 멀티 작업은 병렬적으로 실행되는 것처럼 보이지만 실제로는 번갈아가며 실행되는 동시성 작업입니다.

번갈아가며 실행하는 것이 빠르기 때문에 병렬성으로 처리되는 것처럼 느껴지는 것입니다.

 

 

병렬성은 데이터 병렬성과 작업 병렬성으로 구분할 수 있습니다.

 

데이터 병렬성

전체 데이터를 나누고 서브 데이터들로 만들어서 이 서브 데이터들을 병렬 처리로 작업을 빨리 끝내는 것을 의미합니다.

자바 8에서 지원하고 있는 병렬 스트림은 데이터 병렬성을 구현한 것입니다.

 

멀티 코어 개수만큼 대용량 요소를 서브 요소로 나누고 각 서브 요소마다 분리된 스레드에서 병렬 처리를 진행합니다.

쿼드코어 CPU는 4개의 서브 요소로 나누고 4개의 스레드가 각 서브 요소들을 병렬 처리합니다.

 

작업 병렬성

작업 병렬성은 서로 다른 작업을 병렬로 처리합니다. 웹 서버가 대표적인 예시로 각 브라우저에서 요청한 내용을 개별 스레드에서 병렬로 처리합니다.

 

 

포크조인(Fork Join) 프레임워크

병렬 스트림은 요소들을 병렬 처리하기 위해서 포크조인 프레임워크를 사용합니다.

병렬 스트림을 이용하여 런타임 시 포크조인 프레임워크가 동작합니다.

 

포크 단계에서는 전체 데이터를 서브 데이터로 분리합니다.

서브 데이터는 멀티 코어에서 병렬로 처리합니다.

 

조인 단계에서는 서브 결과를 결합하고 최종 결과를 만듭니다.

 

쿼드 코어 CPU 라면 병렬 스트림으로 작업을 처리하면 스트림 요소를 N개라고 할 때 포크 단계에서 전체 요소를 4등분을 합니다.

나눠진 1등분씩 개별 코어에서 처리하고 조인 단계에서 3번의 결합 과정을 통해 최종 결과를 만듭니다.

 

요소 1 ... N

요소 1/4N, 1/4N, 1/4N, 1/4N

 

서브 요소로 나누는 과정은 알고리즘을 통해 나눠집니다. 포크조인 프레임워크는 포크와 조인 기능 외에도 스레드풀인 ForkJoinPool을 제공합니다. 각 코어에서 서브 요소를 처리하는 것은 개별 스레드가 해야 하기 때문에 스레드 관리가 필요합니다.

 

포크조인 프레임워크는 ExecutorService 구현 객체인 ForkJoinPool을 사용해서 작업 스레드를 관리할 수 있습니다.

 

 

병렬 스트림 생성

병렬 스트림을 이용할 경우 백그라운드에서 포크조인 프레임워크가 사용되므로 개발자가 쉽게 병렬 처리를 할 수 있습니다.

인터페이스 리턴타입 메서드
java.util.Collection Stream parrallelStream()
java.util.Stream.Stream
java.util.Stream.IntStream
java.util.Stream.LongStream
java.util.Stream.DoubleStream
Stream
IntStream
LongStream
DoubleStream
parallell()

 

parrallelStream() 메서드는 컬렉션으로부터 병렬 스트림을 바로 리턴합니다.

parralle() 메서드는 순차 처리 스트림을 병렬 처리 스트림으로 변환해 리턴합니다.

 

어떤 방법으로 병렬 스트림을 얻더라도 이후 요소 처리 과정은 병렬로 처리됩니다. 내부적으로 전체 요소를 서브 요소로 나누고 이 서브 요소들을 개별 스레드가 처리합니다.

 

서브 처리 결과가 나오면 결합하여 최종 결과를 리턴합니다.

 

MaleStudent male = totalList.stream()
    .filter(s -> s.getSex() == Student.Sex.MALE)
    .collect(MaleStudent :: new MaleStudent :: accumulate, MaleStudent :: combine);

 

일반적인 남학생을 추출하는 코드입니다.

전체 학생 목록에서 stream() 메서드로 순차 처리 스트림을 얻습니다.

객체는 하나만 생성되고 남학생을 수집하기 위해 accumulate() 메서드가 호출됩니다.

 

combine() 메서드는 전혀 호출되지 않았는데 그 이유는 순차처리 스트림이므로 결합할 서브 작업이 없기 때문입니다.

위에 코드를 병렬처리 스트림으로 변경하면 다음과 같습니다.

MaleStudent male = totalList.parallelStream()
    .filter(s -> s.getSex() == Student.Sex.MALE)
    .collect(MaleStudent :: new MaleStudent :: accumulate, MaleStudent :: combine);

 

병렬 처리 성능

스트림 병렬 처리가 스트림 순차 처리보다 항상 실행 성능이 좋다고 판단해서는 안됩니다. 병렬 처리에 미치는 영향 3가지 요인

 

1.요소의 수와 요소당 처리 시간

컬렉션에 요소 수가 적고 요소당 처리 시간이 짧다면 순차 처리가 병렬 처리보다 빠를 수 있습니다.

병렬 처리는 스레드풀 생성과 스레드 생성이라는 추가 비용이 발생하기 때문입니다.

 

 

2. 스트림 소스 종류

ArrayList, 배열은 인덱스로 요소를 관리하여 포크 단계에서 요소를 쉽게 분리하므로 병렬 처리 시간이 절약됩니다.

반면 HashSet, TreeSet은 요소 분리가 쉽지 않고, LinkedList도 링크를 따라가야 해서 요소 분리가 쉽지 않습니다.

그래서 이 소스들은 ArrayList, 배열보다는 상대적으로 병렬 처리가 늦습니다.

 

3. 코어의 수

싱글 코어 CPU 경우 순차 처리가 빠릅니다. 병렬 스트림을 사용할 경우 스레드 수만 증가하고 동시성 작ㅇ버으로 처리되어 결과에 좋지 못합니다. 코어의 수가 많을수록 병렬 작업 처리 속도는 빨라집니다.

 

관련글 더보기

댓글 영역