Chapter07 : 병렬 데이터 처리와 성능

  • 컬렉션에 parallelStream 을 호출하면 병렬 스트림이 생성
  • stream : parallelStream (병렬스트림화)
  • parallelStream : sequential (순차스트림화)

스트림의 parallel 의 경우에 병렬로 작업하는 스레드는 어디서 생성되고 몇개까지 생성되는지는 확인해봐야 한다. 일단 기본적인 값은 아래의 명령어를 자바단에서 코드로 확인할 수 있다.

// JVM 에서 가용할 수 있는 최대 스레드 개수를 반환한다.
// 나의 PC 에서는 4개가 나온다.
Runtime.getRuntime().availableProcessors()

현재 내 노트북의 윈도우 CPU 코어확인

윈도우에서 CPU 코어를 확인하면 CPU0 ~ CPU3 까지 있는데 이게 가용가능한 스레드 개수 4개와 일치한다.

  • 병렬 스트림이 무조건적인 대안이 될 수 없다.
  • 만약에 값의 상태를 보유하는 객체에 대해서. 병렬 스트림을 수행할 시 race condition 이 발생한다. 이를 방지하기 위해 syncronized 를 취하지만 이렇게 하면 결국 순차 스트림을 수행한 것과 동일하게 된다.
  • forkJoinTask 라는 것이 있다.
    • forkJoinTask 라는 것은 분할정복기법과 유사하다.
    • 1 부터 10000 까지의 합계 값을 forkJoinTask 기법을 통해 구한다고 가정한다면, 임계치 값을 정해서 그 임계치까지 도달하지 못하면 계속 서브태스크를 나누고, 임계치 값에 도달하면 해당 태스크에 있는 범위의 값들을 합쳐나가는 것. (아래의 코드로 설명하자.)

 

ForkJoinTask Example Code ( 1 부터 N 까지의 합계를 구하기 위한 포크조인 기법 )

/**
 * 스레드 풀을 이용하기 위해선 RecursiveTask<R> 을 상속받아야 한다.
 */
public class ForkJoinSumCalculator extends java.util.concurrent.RecursiveTask<Long> {

    private final long[] numbers;
    private final int start;
    private final int end;

    public static final long THRESHOLD = 10_000;    // 임계점 : 해당 값 이하로는 태스크 분리를 할 수 없다.

    public ForkJoinSumCalculator(long[] numbers) {
        this(numbers, 0, numbers.length);
    }

    private ForkJoinSumCalculator(final long[]numbers, final int start, final int end){
        this.numbers = numbers;
        this.start = start;
        this.end = end;
    }

    /**
     * RecursiveTask 의 경우는 분할정복 형태로 값을 계산함.
     * - 태스크를 임계치에 도달할 때까지 계속 서브태스크로 나눈다.
     * - 임계치로 나눌 수 없는 수준에서는 해당 태스크 내에서 계산을 수행한다.
     * @return
     */
    @Override
    protected Long compute() {

        final int length = end - start;

        if(length <= THRESHOLD) {
            return computeSequentially();
        }

        ForkJoinSumCalculator leftSubTask = new ForkJoinSumCalculator(numbers, start, start + length / 2);

        // ForkJoinPool 의 다른 스레드로 새로 생성한 태스크를 비동기로 실행한다.
        leftSubTask.fork();

        ForkJoinSumCalculator rightSubTask = new ForkJoinSumCalculator(numbers, start + length / 2, end);
        Long rightResult = rightSubTask.compute();  // 두번째 태스크를 동기실행한다.
        Long leftResult = leftSubTask.join();       // 첫번째 태스크 결과를 읽거나 또는 결과가 없을 시 기다린다.

        return rightResult + leftResult;
    }

    private long computeSequentially(){
        long sum = 0;
        for(int i = start; i < end; i++){
            sum += numbers[i];
        }
        return sum;
    }
}

 

메인클래스 ( 메인메소드 포함 )

public class ParallelStreamPractice {
    public static void main(String[] args) {

        System.out.println(forkJoinSum(10000000L));
    }

    private static long forkJoinSum(final long n){
        long[] numbers = LongStream.rangeClosed(1, n).toArray();
        ForkJoinTask<Long> task = new ForkJoinSumCalculator(numbers);

        // RecursiveTask 내에서는 invoke() 메소드를 사용하면 안된다.
        // 내부에서는 fork() 나 compute() 를 직접 호출할 수 있어야 한다.
        return new ForkJoinPool().invoke(task);
    }
}

 

-

병렬 프로그래밍 관련해서는, 여기까지.

 

Chapter08 : 리팩토링, 테스팅, 디버깅

 

1. 익명 클래스를 람다 표현식으로 리팩토링하기.

final Runnable runnable = new Runnable() {
    @Override
    public void run() {
        System.out.println("run !");
    }
};

final Runnable refactorRunnable = () -> System.out.println("refactor Run !");

runnable.run();
refactorRunnable.run();

위처럼 작성이 가능하다. 다만, 메소드 오버로딩이 들어간 상태에서 람다표현식을 쓰려고 한다면 동일 시그니처를 가진 함수에 대해서 대상형식에 대한 모호함이 발생한다.

 

모호함이 발생하는 부분.

/** 모호함이 발생한다. **/
doSomething(() -> System.out.println(">>"));

public static void doSomething(Task task){
    task.execute();
}

public static void doSomething(Runnable runnable){
    runnable.run();;
}

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

@FunctionalInterface
public interface Task{
    public void execute();
}

doSomething(() -> System.out.println(">>"); 의 구문을 doSomething((Task) -> System.out.println(">")); 의 형식으로 작성해주어야 한다. 대상형식이 필요하다. 인텔리제이 IDE 에서는 컴파일 에러가 발생하면서 해당 구문을 자동으로 수정해준다.

 

 

2. 람다표현식을 메소드 레퍼런스로 리팩토링하기.

람다를 작성하게 되면 표현상의 이점이 있지만 직관적인 코드가 되기 어렵다. 그래서 메소드 레퍼런스를 통해서 해당 로직의 구문이 어떤 의도를 가지고 작성되고 있는지를 파악할 수 있다.

 

3. 람다 테스팅

람다를 테스트한다면 람다를 사용하는 메소드 동작에 집중해서 테스트해야 한다. 람다를 테스트하는 것이 아니라 람다를 래핑하고 있는 메소드를 테스트하는 것이 바람직하다.

 

4. 람다 디버깅

람다 표현식을 디버깅하기란 어렵다. 스택트레이스를 보아도 명확하지 않다. 반면에 메소드참조로 걸어두면 해당 메소드에서 에러를 발견할 수 있어 상대적으로 쉽다. 다만 람다 디버깅 부분은 미래의 자바 컴파일러가 개선해야할 부분이라고 책에 쓰여있다.

Posted by doubler
,