스레드 풀과 Fork / Join의 궁극적 인 목표는 모두 같습니다. 둘 다 처리량을 최대화하기 위해 최대한 사용 가능한 CPU 성능을 활용하려고합니다. 최대 처리량은 가능한 많은 작업을 장기간 완료해야 함을 의미합니다. 그렇게하려면 무엇이 필요합니까? (다음은 계산 작업이 부족하지 않다고 가정합니다. 100 % CPU 사용에는 항상 충분한 양이 있습니다. 또한 하이퍼 스레딩의 경우 코어 또는 가상 코어에 대해 "CPU"를 동일하게 사용합니다).
- 최소한의 스레드를 실행하면 코어가 사용되지 않기 때문에 사용 가능한 CPU 수만큼 스레드를 실행해야합니다.
- 더 많은 스레드를 실행하면 다른 스레드에 CPU를 할당하는 스케줄러에 추가로드가 발생하여 일부 CPU 시간이 계산 작업이 아닌 스케줄러로 이동하기 때문에 최대한 많은 스레드가 실행 중이어야합니다.
따라서 우리는 최대 처리량을 위해 CPU와 정확히 같은 수의 스레드가 필요하다는 것을 알았습니다. Oracle의 모호한 예에서 사용 가능한 CPU 수와 동일한 스레드 수로 고정 크기 스레드 풀을 사용하거나 스레드 풀을 사용할 수 있습니다. 차이가 없습니다, 당신 말이 맞아요!
그렇다면 언제 스레드 풀에 문제가 생길까요? 스레드가 다른 작업이 완료되기를 기다리고 있기 때문에 스레드가 차단되는 경우 입니다. 다음 예제를 가정하십시오.
class AbcAlgorithm implements Runnable {
public void run() {
Future<StepAResult> aFuture = threadPool.submit(new ATask());
StepBResult bResult = stepB();
StepAResult aResult = aFuture.get();
stepC(aResult, bResult);
}
}
여기서 볼 수있는 것은 3 단계 A, B 및 C로 구성된 알고리즘입니다. A와 B는 서로 독립적으로 수행 될 수 있지만 C 단계는 단계 A와 B의 결과가 필요합니다.이 알고리즘이하는 일은 작업 A를 제출하는 것입니다 스레드 풀과 태스크 b를 직접 수행하십시오. 그런 다음 스레드는 작업 A도 완료 될 때까지 기다렸다가 단계 C를 계속합니다. A와 B가 동시에 완료되면 모든 것이 정상입니다. 그러나 A가 B보다 오래 걸리면 어떻게 될까요? 작업 A의 특성이이를 지시하기 때문일 수 있지만, 처음에 사용 가능한 작업 A에 대한 스레드가없고 작업 A가 대기해야하기 때문일 수도 있습니다. (사용 가능한 단일 CPU가 있고 스레드 풀에 단일 스레드 만있는 경우 교착 상태가 발생할 수 있지만 현재로서는 문제가 아닙니다.) 요점은 작업 B를 방금 실행 한 스레드가전체 스레드를 차단합니다 . CPU와 동일한 수의 스레드가 있고 하나의 스레드가 차단되므로 하나의 CPU가 유휴 상태 임을 의미합니다 .
포크 / 조인이이 문제를 해결합니다. 포크 / 조인 프레임 워크에서 다음과 같은 알고리즘을 작성합니다.
class AbcAlgorithm implements Runnable {
public void run() {
ATask aTask = new ATask());
aTask.fork();
StepBResult bResult = stepB();
StepAResult aResult = aTask.join();
stepC(aResult, bResult);
}
}
동일하게 보이지 않습니까? 그러나 단서는 aTask.join
차단되지 않습니다 . 대신, 여기서는 작업 스털링 이 시작됩니다. 스레드는 과거에 포크 된 다른 작업을 둘러보고 계속 진행할 것입니다. 먼저 분기 된 작업이 처리를 시작했는지 확인합니다. 따라서 A가 다른 스레드에 의해 아직 시작되지 않은 경우 다음에 A를 수행하고 그렇지 않으면 다른 스레드의 큐를 확인하고 작업을 도용합니다. 다른 스레드의 다른 작업이 완료되면 A가 지금 완료되었는지 확인합니다. 위의 알고리즘이라면를 호출 할 수 있습니다 stepC
. 그렇지 않으면 훔칠 또 다른 작업을 찾습니다. 따라서 포크 / 조인 풀은 차단 작업에도 불구하고 100 % CPU 사용률을 달성 할 수 있습니다 .
그러나 함정이 있습니다. 작업 도청은 s 의 join
호출 에만 가능합니다 ForkJoinTask
. 다른 스레드 대기 또는 I / O 조치 대기와 같은 외부 차단 조치에는 수행 할 수 없습니다. 그렇다면 I / O가 완료되기를 기다리는 것은 일반적인 작업입니까? 이 경우 차단 작업이 완료 되 자마자 다시 중지되는 추가 스레드를 포크 / 조인 풀에 추가 할 수 있다면 두 번째로 가장 좋은 방법입니다. 그리고 ForkJoinPool
우리가 ManagedBlocker
s를 사용한다면 실제로 그렇게 할 수 있습니다 .
피보나치
에서 RecursiveTask 용의 JavaDoc 포크 / 가입하여 피보나치 수를 산출하기위한 일례이다. 클래식 재귀 솔루션은 다음을 참조하십시오.
public static int fib(int n) {
if (n <= 1) {
return n;
}
return fib(n - 1) + fib(n - 2);
}
JavaDocs에서 설명했듯이 피보나치 수를 계산하는 덤프 방법입니다.이 알고리즘은 복잡도가 O (2 ^ n)이며 간단한 방법이 가능하기 때문입니다. 그러나이 알고리즘은 매우 간단하고 이해하기 쉽기 때문에이 알고리즘을 고수합니다. 포크 / 조인으로 속도를 높이고 싶다고 가정 해 봅시다. 순진한 구현은 다음과 같습니다.
class Fibonacci extends RecursiveTask<Long> {
private final long n;
Fibonacci(long n) {
this.n = n;
}
public Long compute() {
if (n <= 1) {
return n;
}
Fibonacci f1 = new Fibonacci(n - 1);
f1.fork();
Fibonacci f2 = new Fibonacci(n - 2);
return f2.compute() + f1.join();
}
}
이 작업이 분리되는 단계는 너무 짧아서 너무 끔찍하게 수행되지만 프레임 워크가 일반적으로 어떻게 잘 작동하는지 볼 수 있습니다. 두 개의 summand는 독립적으로 계산할 수 있지만 최종 구성하려면 두 가지가 필요합니다. 결과. 따라서 절반은 다른 스레드에서 수행됩니다. 교착 상태를 갖지 않고도 스레드 풀에서 동일한 작업을 수행 할 수 있습니다 (단순하지는 않지만).
완전성을 위해 :이 재귀 접근법을 사용하여 피보나치 수를 실제로 계산하려면 여기에 최적화 된 버전이 있습니다.
class FibonacciBigSubtasks extends RecursiveTask<Long> {
private final long n;
FibonacciBigSubtasks(long n) {
this.n = n;
}
public Long compute() {
return fib(n);
}
private long fib(long n) {
if (n <= 1) {
return 1;
}
if (n > 10 && getSurplusQueuedTaskCount() < 2) {
final FibonacciBigSubtasks f1 = new FibonacciBigSubtasks(n - 1);
final FibonacciBigSubtasks f2 = new FibonacciBigSubtasks(n - 2);
f1.fork();
return f2.compute() + f1.join();
} else {
return fib(n - 1) + fib(n - 2);
}
}
}
이것은 서브 태스크 n > 10 && getSurplusQueuedTaskCount() < 2
가 참일 때만 분할되기 때문에 서브 태스크를 훨씬 더 작게 유지합니다. 즉, 수행해야 할 메소드 호출이 100 개를 훨씬 초과 n > 10
하고 ( ) 이미 대기중인 수동 태스크가 없습니다 ( getSurplusQueuedTaskCount() < 2
).
내 컴퓨터 (4 코어 (하이퍼 스레딩 계산시 8 개), 인텔 ® 코어 ™ i7-2720QM CPU (2.20GHz) fib(50)
에서는 64 초, 클래식 접근 방식에서는 64 초, 포크 / 조인 방식에서는 18 초 이론적으로 가능한 한 많지는 않지만 상당히 눈에 띄는 이익입니다.
요약
- 예, 예에서 포크 / 조인은 클래식 스레드 풀보다 이점이 없습니다.
- 포크 / 조인은 차단과 관련하여 성능을 크게 향상시킬 수 있습니다
- 포크 / 가입은 일부 교착 상태 문제를 피합니다