Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Chapter 16] List의 크기가 스레드 수와 같을 때 CompletableFuture가 2배 느린 이유 #16

Open
whquddn55 opened this issue Nov 30, 2022 · 0 comments

Comments

@whquddn55
Copy link
Member

whquddn55 commented Nov 30, 2022

출처

p.512 아래
두 가지 버전 모두 내부적으로 Runtime.getRuntime().availableProcessors()가 반환하는 스레드 수를 사용...

질문

책에서는 parellelStreamCompletableFuture가 모두 Runtime.getRuntime().availableProcessors()를 사용한다고 언급하고 있습니다.
하지만, 당장 책에서 나온 예제에서 pc의 가용 스레드가 4개일 때 4 크기의 list를 병렬로 실행하면 parellelStrema은 1초가 걸리는 반면, CompletableFuture는 2초가 걸리게 됩니다.

스레드 수 만큼 병렬 실행한다면 같이 1초가 걸려야하는 것이 맞습니다. list 크기가 5일 때 둘 다 2초가 걸리니까요. 책에서 세팅없이 실행했을 때 CompletableFuture는 왜 2초가 걸리는지에 대한 언급이 있지는 않습니다.
구글링해봐도 CompletableFuture가 default세팅으로는 느리기 때문에 Executor로 설정해주어야 더 빠르게 할 수 있다는 언급만 있습니다.

가능성은 하나 뿐이라서 테스트를 해봤습니다.

  • CompletableFuture의 default config이 스레드수 만큼 생성하지 않는다.

여러번 테스트 해본 이후 결론은 아래와 같습니다.

  • CompletableFuture뿐만 아니라 'ParellelStream'도 (가용 스레드 수 - 1)개 만큼 스레드를 생성한다.
  • ParellelStream은 해당 코드를 실행한 호출 스레드에서도 실행하기 때문에 최종적으로 (가용 스레드 수)개 만큼 스레드를 동작시킨다.
  • CompletableFuture은 호출 스레드에서는 실행하지 않기 때문에 최종적으로 (가용 스레드 수 - 1)개 만큼 스레드를 동작시킨다.

테스트 과정은 아래와 같습니다.

CompletableFuture의 기본 설정

System.out.printf("Before: %d\n", Thread.activeCount());
List<CompletableFuture<String>> priceFutures = shops.stream().map(shop -> CompletableFuture.supplyAsync(
        () -> String.format("Total: %d, current: %d, %.2f\n", Thread.activeCount(), Thread.currentThread().getId(), shop.getPrice(product)))).collect(
        Collectors.toList());

코드가 보기 어려운데, 대충 매번 stream의 원소를 계산할 때 마다 활성화된 Thread의 개수를 출력하는 코드입니다. 출력은 아래와 같습니다.

Before: 2
[Total: 6, current: 22, 230.08
, Total: 6, current: 23, 230.61
, Total: 7, current: 25, 230.17
, Total: 9, current: 24, 230.82
, Total: 9, current: 26, 230.96
, Total: 11, current: 27, 230.68
, Total: 11, current: 28, 230.58
, Total: 13, current: 29, 230.42
, Total: 13, current: 30, 230.99
, Total: 15, current: 31, 230.08
, Total: 15, current: 32, 230.48
, Total: 17, current: 33, 230.46
, Total: 17, current: 34, 230.21
, Total: 17, current: 35, 230.82
, Total: 17, current: 36, 230.76
, Total: 17, current: 35, 230.88
]
Done in 2064 msecs

테스트한 pc의 스레드 수가 16개라 list크기를 16으로 잡고 돌렸습니다. 실행하기 전 스레드 수는 2개였고, 마지막 실행에서 확인한 스레드 수는 17개로 15개가 늘었습니다. 즉, 16개가 아닌 15로 설정되어있다고 예상할 수 있습니다.
실제로 current 스레드를 보면 35가 2개 나와서 35번째 스레드에서 두 개를 실행함을 알 수 있습니다.

CompletableFuture에서 Executor를 수동 설정할 경우

System.out.printf("Before: %d\n", Thread.activeCount());
ExecutorService executorService = Executors.newFixedThreadPool(16);
List<CompletableFuture<String>> priceFutures = shops.stream().map(shop -> CompletableFuture.supplyAsync(
        () -> String.format("Total: %d, current: %d, %.2f\n", Thread.activeCount(), Thread.currentThread().getId(), shop.getPrice(product)), executorService)).collect(
        Collectors.toList());
executorService.shutdown();

단순하게 Thread수를 16개로 고정시킨 후 실행한 코드입니다.

Before: 2
[Total: 5, current: 22, 230.16
, Total: 5, current: 23, 230.42
, Total: 6, current: 24, 230.85
, Total: 8, current: 25, 230.91
, Total: 9, current: 26, 230.81
, Total: 9, current: 27, 230.88
, Total: 10, current: 28, 230.65
, Total: 11, current: 29, 230.54
, Total: 12, current: 30, 230.77
, Total: 13, current: 31, 230.45
, Total: 14, current: 32, 230.82
, Total: 15, current: 33, 230.81
, Total: 17, current: 34, 230.07
, Total: 17, current: 35, 230.91
, Total: 18, current: 36, 230.25
, Total: 18, current: 37, 230.52
]
Done in 1038 msecs

결과를 보면 최종적으로 (18 - 2)개로 16개의 Thread를 생성함을 알 수 있습니다. 간단한 예제지만, CompletableFuture가 확실히 Thread수만큼 생성하지 않음을 알 수 있습니다.

ParellelStream의 기본 설정

System.out.printf("Before: %d\n", Thread.activeCount());
return shops.parallelStream()
        .map(shop -> String.format("Total: %d, current: %d, %.2f\n", Thread.activeCount(), Thread.currentThread().getId(), shop.getPrice(product)))
            .collect(Collectors.toList());
Before: 2
[Total: 17, current: 35, 230.60
, Total: 17, current: 34, 230.34
, Total: 14, current: 25, 230.95
, Total: 17, current: 32, 230.33
, Total: 16, current: 30, 230.31
, Total: 17, current: 22, 230.46
, Total: 17, current: 36, 230.43
, Total: 17, current: 26, 230.92
, Total: 17, current: 29, 230.78
, Total: 17, current: 31, 230.41
, Total: 4, current: 1, 230.40
, Total: 17, current: 33, 230.64
, Total: 12, current: 28, 230.73
, Total: 14, current: 24, 230.76
, Total: 17, current: 23, 230.71
, Total: 15, current: 27, 230.62
]
Done in 1028 msecs

ParellelStream도 (17 - 2) = 15개의 스레드를 생성하는 것을 볼 수 있지만, 중간에 current 스레드의 Id가 1인 것을 하나 발견할 수 있습니다. 즉, 현재 parelleStream을 실행한 코드에서도 같은 동작을 실행하는 것을 알 수 있습니다.
실제로 디버깅용 출력문을 몇 개 추가하고 실행해보면 메인 스레드에서도 1초간 기다린 후 결과가 나오는 것을 알 수 있습니다.

결론

  • ParellelStreamCompletableFuture 모두 ForkJoinPool.commonPool() 만큼의 스레드 수를 생성한다.
  • ForkJoinPool.commonPool()Runtime.getRuntime().availableProcessors() - 1 만큼을 의미한다. 즉, 가용 스레드 수 - 1 만큼을 의미한다.
  • ParellelStream은 메인 스레드를 포함하여 생성한 스레드를 실행하기 때문에 Runtime.getRuntime().availableProcessors() 만큼의 스레드를 실행한다.
  • CompletableFuture은 메인 스레드를 포함하지 않기 때문에 Runtime.getRuntime().availableProcessors() - 1 만큼의 스레드를 실행한다.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant