포크 / 조인 프레임 워크가 스레드 풀보다 나은 점은 무엇입니까?


134

처음에는 큰 작업을 N 개의 하위 작업으로 나누고 ( Executors 의 캐시 된 스레드 풀로 ) 각 작업이 완료되기를 기다리는 것보다 새로운 fork / join 프레임 워크 를 사용하면 어떤 이점이 있습니까? 포크 / 조인 추상화를 사용하여 문제를 단순화하거나 현재 몇 년 동안 솔루션을보다 효율적으로 만드는 방법을 알지 못합니다.

예를 들어, 튜토리얼 예제 의 병렬화 된 흐림 알고리즘은 다음 과 같이 구현 될 수 있습니다.

public class Blur implements Runnable {
    private int[] mSource;
    private int mStart;
    private int mLength;
    private int[] mDestination;

    private int mBlurWidth = 15; // Processing window size, should be odd.

    public ForkBlur(int[] src, int start, int length, int[] dst) {
        mSource = src;
        mStart = start;
        mLength = length;
        mDestination = dst;
    }

    public void run() {
        computeDirectly();
    }

    protected void computeDirectly() {
        // As in the example, omitted for brevity
    }
}

처음에 분할하여 작업을 스레드 풀로 보냅니다.

// source image pixels are in src
// destination image pixels are in dst
// threadPool is a (cached) thread pool

int maxSize = 100000; // analogous to F-J's "sThreshold"
List<Future> futures = new ArrayList<Future>();

// Send stuff to thread pool:
for (int i = 0; i < src.length; i+= maxSize) {
    int size = Math.min(maxSize, src.length - i);
    ForkBlur task = new ForkBlur(src, i, size, dst);
    Future f = threadPool.submit(task);
    futures.add(f);
}

// Wait for all sent tasks to complete:
for (Future future : futures) {
    future.get();
}

// Done!

작업은 스레드 풀의 대기열로 이동하여 작업자 스레드가 사용 가능 해지면 실행됩니다. 분할이 충분히 세분화되고 (특히 마지막 작업을 기다릴 필요가 없도록) 스레드 풀에 충분한 (최소 N 개의 프로세서) 스레드가 있으면 모든 프로세서는 전체 계산이 완료 될 때까지 최고 속도로 작동합니다.

뭔가 빠졌습니까? 포크 / 조인 프레임 워크를 사용하면 어떤 부가 가치가 있습니까?

답변:


136

기본적인 오해는 포크 / 조인 예제가 업무 도용을 보여 주지 않고 일종의 표준 나누기와 정복 만 보여주는 것이라고 생각합니다 .

작업 도용은 다음과 같습니다. 작업자 B가 작업을 완료했습니다. 그는 친절한 사람이므로 주위를 둘러보고 작업자 A가 여전히 열심히 일하는 것을 봅니다. 그는 걸어 다니며 물었다. "이봐, 내가 손을 줄 수있어." 답글입니다. "쿨, 나는 1000 단위 의이 작업을했습니다. 지금까지 나는 345 떠나 655를 완료했습니다. 당신은 번호 673에서 1000에 대해 작업하십시오, 346에서 672를 할 것입니다." B는 "좋아요, 먼저 술집에 갈 수 있도록 시작하겠습니다"라고 말합니다.

알다시피-노동자는 실제 작업을 시작할 때도 서로 의사 소통해야합니다. 이것은 예제에서 빠진 부분입니다.

반면에 예는 "하청 업체 사용"과 같은 것만 보여줍니다.

Worker A : "Dang, 나는 1000 단위의 일을하고있다. 나에게 너무 많은 일이다. 나는 500을 스스로하고 500을 하도급 할 것이다." 이는 큰 작업이 각각 10 개 단위의 작은 패킷으로 분류 될 때까지 계속됩니다. 이들은 가능한 노동자들에 의해 처형 될 것입니다. 그러나 하나의 패킷이 일종의 독약이고 다른 패킷보다 상당히 오래 걸리면 (불운) 분할 단계는 끝납니다.

Fork / Join과 작업을 미리 분할하는 것의 유일한 차이점은 다음과 같습니다. 미리 분할 할 때 작업 큐가 시작부터 바로 가득 찼습니다. 예 : 1000 단위, 임계 값은 10이므로 큐에 100 개의 항목이 있습니다. 이 패킷은 스레드 풀 멤버에 분배됩니다.

포크 / 조인은 더 복잡하며 큐의 패킷 수를 더 작게 유지하려고합니다.

  • 1 단계 : (1 ... 1000)을 포함하는 하나의 패킷을 대기열에 넣습니다.
  • 2 단계 : 한 작업자가 패킷을 팝 (1 ... 1000)하여 두 개의 패킷 (1 ... 500) 및 (501 ... 1000)으로 바꿉니다.
  • 3 단계 : 한 근로자가 패킷 (500 ... 1000)을 팝하고 (500 ... 750) 및 (751 ... 1000)을 푸시합니다.
  • n 단계 : 스택에는 (1..500), (500 ... 750), (750 ... 875) ... (991..1000) 패킷이 포함됩니다.
  • n + 1 단계 : 패킷 (991..1000)이 팝되어 실행됩니다.
  • 단계 n + 2 : 패킷 (981..990)이 팝되어 실행됩니다
  • 단계 n + 3 : 패킷 (961..980)이 팝되어 (961 ... 970) 및 (971..980)으로 분할됩니다. ....

다음을 참조하십시오. 포크 / 조인에서 큐가 더 작고 (예에서 6) "분할"및 "작업"단계가 인터리브됩니다.

여러 근로자가 동시에 튀어 나오면서 밀릴 때 상호 작용은 분명하지 않습니다.


나는 이것이 실제로 답이라고 생각합니다. 작업 도용 기능도 보여주는 실제 Fork / Join 예제가 있는지 궁금합니다. 기본 예제를 통해 작업량을 장치의 크기 (예 : 배열 길이)에서 완벽하게 예측할 수 있으므로 사전 분할이 쉽습니다. 도난은 유닛 당 워크로드의 양 이 유닛의 크기와 잘 예측할 수 없는 문제를 확실히 변화시킬 것 입니다.
Joonas Pulakka

AH 귀하의 답변이 정확하다면, 그 방법을 설명하지 않습니다. Oracle이 제공 한 예는 작업 도용을 초래하지 않습니다. 여기서 설명하는 예에서와 같이 포크와 조인은 어떻게 작동합니까? 포크와 조인이 설명하는 방식으로 작동하는 Java 코드를 보여줄 수 있습니까? 감사합니다
Marc

@Marc : 죄송합니다. 사용 가능한 예가 없습니다.
AH

6
Oracle의 예제 인 IMO의 문제점은 작업 도용 (AH에서 설명한 것처럼)을 보여주지 않고 단순한 Joonas와 마찬가지로 간단한 ThreadPool에 대한 알고리즘을 코딩하기 쉽다는 것입니다. FJ는 작업이 충분한 독립 작업으로 사전 분할 될 수 없지만 독립적 인 작업으로 재귀 적으로 분할 될 수있는 경우에 가장 유용합니다. 예를 들어 내 답변보기
ashirley

2
작업 도용이 유용한 몇 가지 예 : h-online.com/developer/features/…
volley

27

사용중인 스레드가 모두 100 %로 독립적으로 작동하는 경우에는 포크 조인 (FJ) 풀의 n 스레드보다 낫습니다. 그러나 결코 그런 식으로 작동하지 않습니다.

문제를 n 개의 동일한 조각으로 정확하게 분할하지 못할 수 있습니다. 그럼에도 불구하고 스레드 스케줄링은 공정하지 않은 방법입니다. 가장 느린 스레드를 기다리게됩니다. 여러 작업이있는 경우 각각 n-way 병렬 처리 (일반적으로 더 효율적)로 실행할 수 있지만 다른 작업이 완료되면 n-way로 올라갈 수 있습니다.

그렇다면 문제를 FJ 크기로 잘라서 스레드 풀 작업을 해보는 것이 어떻습니까? 일반적인 FJ 사용법은 문제를 작은 조각으로 줄입니다. 이를 무작위 순서로 수행하려면 하드웨어 수준에서 많은 조정이 필요합니다. 오버 헤드는 살인자 일 것입니다. FJ에서 태스크는 스레드가 LIFO / 스택 (Last In First Out) 순서로 읽는 큐에 배치되며, 작업 도용 (핵심 작업의 경우)은 선입 선출 (FIFO / "대기열)입니다. 결과적으로 긴 배열 처리는 작은 덩어리로 나눠 지더라도 크게 순차적으로 수행 될 수 있습니다. (한 빅뱅에서 작은 크기의 덩어리로 문제를 나누는 것이 사소한 일이 아닌 경우도 있습니다. 균형없이 어떤 형태의 계층 구조를 다루는 것을 말합니다.)

결론 : FJ를 사용하면 고르지 않은 상황에서 하드웨어 스레드를보다 효율적으로 사용할 수 있습니다. 스레드가 두 개 이상인 경우 항상 그렇습니다.


그러나 왜 FJ가 가장 느린 스레드를 기다리지 않습니까? 미리 결정된 수의 하위 작업이 있으며 물론 그 중 일부는 항상 마지막 작업 일 것입니다. maxSize내 예제 에서 매개 변수를 조정하면 FJ 예제의 "이진 분할"과 거의 유사한 하위 작업 분할이 생성됩니다 ( compute()메서드에서 무언가를 계산하거나 하위 작업을 전송 하는 메서드 내에서 수행됨 invokeAll()).
Joonas Pulakka

그것들은 훨씬 작기 때문에-내 대답에 추가 할 것입니다.
Tom Hawtin-tackline

하위 작업의 수가 실제로 병렬로 처리 할 수있는 것보다 크면 (마지막을 기다리지 않아도 됨) 조정 문제를 볼 수 있습니다. FJ 예제 는 분할이 세분화되어야하는 경우 오도 될 수 있습니다 .1000x1000 이미지의 경우 16500 개의 실제 하위 작업을 생성하여 각 62500 요소를 처리하는 100000의 임계 값을 사용합니다. 10000x10000 이미지의 경우 이미 1024 개의 하위 작업이 있습니다.
Joonas Pulakka

19

스레드 풀과 Fork / Join의 궁극적 인 목표는 모두 같습니다. 둘 다 처리량을 최대화하기 위해 최대한 사용 가능한 CPU 성능을 활용하려고합니다. 최대 처리량은 가능한 많은 작업을 장기간 완료해야 함을 의미합니다. 그렇게하려면 무엇이 필요합니까? (다음은 계산 작업이 부족하지 않다고 가정합니다. 100 % CPU 사용에는 항상 충분한 양이 있습니다. 또한 하이퍼 스레딩의 경우 코어 또는 가상 코어에 대해 "CPU"를 동일하게 사용합니다).

  1. 최소한의 스레드를 실행하면 코어가 사용되지 않기 때문에 사용 가능한 CPU 수만큼 스레드를 실행해야합니다.
  2. 더 많은 스레드를 실행하면 다른 스레드에 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우리가 ManagedBlockers를 사용한다면 실제로 그렇게 할 수 있습니다 .

피보나치

에서 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 초 이론적으로 가능한 한 많지는 않지만 상당히 눈에 띄는 이익입니다.

요약

  • 예, 예에서 포크 / 조인은 클래식 스레드 풀보다 이점이 없습니다.
  • 포크 / 조인은 차단과 관련하여 성능을 크게 향상시킬 수 있습니다
  • 포크 / 가입은 일부 교착 상태 문제를 피합니다

17

포크 / 조인은 작업 도용을 구현하므로 스레드 풀과 다릅니다. 에서 포크 / 가입

다른 ExecutorService와 마찬가지로 fork / join 프레임 워크는 스레드 풀의 작업자 스레드로 작업을 배포합니다. 포크 / 조인 프레임 워크는 워크 스털링 알고리즘을 사용하므로 구별됩니다. 수행 할 작업이 부족한 작업자 스레드는 여전히 사용중인 다른 스레드에서 작업을 훔칠 수 있습니다.

두 개의 스레드와 4 개의 작업 a, b, c, d가 각각 1, 1, 5 및 6 초가 걸린다고 가정 해보십시오. 처음에는 a와 b가 스레드 1에 할당되고 c와 d가 스레드 2에 할당됩니다. 스레드 풀에서는 11 초가 걸립니다. 포크 / 조인을 사용하면 스레드 1이 완료되고 스레드 2에서 작업을 훔칠 수 있으므로 작업 d는 스레드 1에 의해 실행됩니다. 스레드 1은 a, b 및 d를 실행하고 스레드 2는 c 만 실행합니다. 전체 시간 : 11 초가 아닌 8 초

편집 : Joonas가 지적한 것처럼 작업이 스레드에 사전 할당되지는 않습니다. 포크 / 조인의 아이디어는 스레드가 작업을 여러 하위 조각으로 분할하도록 선택할 수 있다는 것입니다. 위의 내용을 다시 말하면 :

우리는 각각 2 초와 11 초가 걸리는 두 가지 작업 (ab)과 (cd)가 있습니다. 스레드 1이 ab를 실행하기 시작하고이를 두 개의 하위 작업 a & b로 나눕니다. 스레드 2와 마찬가지로 두 개의 하위 작업 c & d로 나뉩니다. 스레드 1이 a & b를 완료하면 스레드 2에서 d를 훔칠 수 있습니다.


5
스레드 풀은 일반적으로 ThreadPoolExecutor 인스턴스입니다. 따라서 작업은 대기열 로 이동합니다 ( 실제로는 BlockingQueue ). 작업자 스레드는 이전 작업을 완료하자마자 작업을 수행합니다. 내가 이해하는 한 작업은 특정 스레드에 미리 할당 되지 않았습니다 . 각 스레드에는 한 번에 하나의 작업이 있습니다.
Joonas Pulakka

4
AFAIK에는 하나의 ThreadPoolExecutor에 대한 하나의 큐가 있으며, 이는 여러 스레드를 제어합니다 . 즉, 태스크 또는 실행 가능 항목 (스레드가 아님)을 실행자에 지정하면 해당 태스크도 특정 스레드에 사전 할당되지 않습니다. FJ도 정확히 그렇게합니다. 지금까지 FJ를 사용하면 아무런 이점이 없습니다.
AH

1
@AH 예. 그러나 포크 / 조인을 사용하면 현재 작업을 분할 할 수 있습니다. 작업을 실행하는 스레드는이를 두 개의 다른 작업으로 나눌 수 있습니다. 따라서 ThreadPoolExecutor를 사용하면 고정 된 작업 목록이 있습니다. 포크 / 조인을 사용하면 실행중인 작업이 자신의 작업을 두 개로 나눌 수 있으며, 작업을 마치면 다른 스레드에서 선택할 수 있습니다. 아니면 먼저 끝내면
Matthew Farwell

1
@Matthew Farwell : FJ 예제 에서 각 작업 내에서 작업 compute()을 계산하거나 두 개의 하위 작업으로 나눕니다. 선택하는 옵션 은 작업 크기 ( ) 에만 의존 if (mLength < sThreshold)...하므로 고정 된 수의 작업을 만드는 멋진 방법 일뿐입니다. 1000x1000 이미지의 경우 실제로 무언가를 계산하는 정확히 16 개의 하위 작업이 있습니다. 또한 하위 작업 만 생성하고 호출하며 자체적으로 계산하지 않는 15 개의 (= 16-1) "중간"작업이 있습니다.
Joonas Pulakka

2
@Matthew Farwell : 모든 FJ를 이해하지 못할 수도 있지만 하위 작업이 해당 computeDirectly()메서드 를 실행하기로 결정한 경우 더 이상 아무것도 훔칠 방법이 없습니다. 적어도 예에서는 전체 분할이 우선적으로 수행된다 .
Joonas Pulakka

14

위의 모든 사람은 일 도둑질로 얻을 수있는 이점이 맞지만 이것이 왜 그런지 확장하는 것입니다.

주요 이점은 작업자 스레드 간의 효율적인 조정입니다. 작업을 분할하고 재 조립해야하며 조정이 필요합니다. 위에서 AH의 답변에서 볼 수 있듯이 각 스레드에는 자체 작업 목록이 있습니다. 이 목록의 중요한 속성은 목록이 정렬된다는 것입니다 (위에 큰 작업이 있고 아래쪽에 작은 작업이 있음). 각 스레드는 목록 맨 아래에서 작업을 실행하고 다른 스레드 목록 맨 위에서 작업을 훔칩니다.

이것의 결과는 다음과 같습니다.

  • 작업 목록의 머리와 꼬리는 독립적으로 동기화되어 목록에서 경합을 줄입니다.
  • 작업의 중요한 하위 트리는 동일한 스레드로 분할 및 재 조립되므로 이러한 하위 트리에 대해 스레드 간 조정이 필요하지 않습니다.
  • 실이 훔칠 때 큰 조각을 취한 다음 자체 목록으로 세분화합니다.
  • 가공 강은 나사산이 공정이 끝날 때까지 거의 완전히 활용됨을 의미합니다.

스레드 풀을 사용하는 대부분의 다른 분할 및 정복 체계에는 더 많은 스레드 간 통신 및 조정이 필요합니다.


13

이 예에서 포크 / 조인은 포크가 필요하지 않고 워크로드가 작업자 스레드간에 균등하게 분할되므로 값을 추가하지 않습니다. 포크 / 조인은 오버 헤드 만 추가합니다.

여기 주제에 관한 좋은 기사 가 있습니다. 인용문:

전반적으로 워크로드가 작업자 스레드간에 균등하게 분할되는 경우 ThreadPoolExecutor가 선호된다고 말할 수 있습니다. 이를 보장하려면 입력 데이터의 모양을 정확하게 알아야합니다. 반대로 ForkJoinPool은 입력 데이터에 관계없이 우수한 성능을 제공하므로 훨씬 강력한 솔루션입니다.


8

또 다른 중요한 차이점은 FJ를 사용하면 여러 개의 복잡한 "가입"단계를 수행 할 수 있다는 것입니다. http://faculty.ycp.edu/~dhovemey/spring2011/cs365/lecture/lecture18.html 에서 병합 정렬을 고려하면 이 작업을 사전 분할하는 데 너무 많은 오케스트레이션이 필요합니다. 예를 들어 다음을 수행해야합니다.

  • 1 분기를 정렬하다
  • 2 분기 정렬
  • 첫 2 분기 합병
  • 3 분기 정렬
  • 4 분기를 정렬하다
  • 지난 2 분기 합병
  • 두 반쪽을 병합

병합 등을 수행하기 전에 정렬을 수행하도록 지정하는 방법은 무엇입니까?

각 항목 목록에 대해 특정 작업을 수행하는 최선의 방법을 찾고 있습니다. 목록을 미리 분리하고 표준 ThreadPool을 사용한다고 생각합니다. FJ는 작업이 충분히 독립된 작업으로 사전 분할 될 수 없지만 독립적으로 독립된 작업으로 재귀 적으로 분할 될 수있는 경우에 가장 유용합니다 (예 : 절반을 정렬하는 것은 독립적이지만 2 개의 정렬 된 절반을 정렬 된 전체로 병합하는 것은 아닙니다).


6

F / J는 또한 값 비싼 병합 작업이있을 때 뚜렷한 이점이 있습니다. 트리 구조로 분할되므로 선형 스레드 분할과의 n 병합과 달리 log2 (n) 병합 만 수행합니다. (이것은 스레드만큼 많은 프로세서를 가지고 있다고 이론적으로 가정하지만 여전히 유리합니다.) 숙제를 할당하기 위해 각 인덱스의 값을 합산하여 수천 개의 2D 배열 (모두 동일한 치수)을 병합해야했습니다. 포크 조인 및 P 프로세서를 사용하면 P가 무한대에 접근함에 따라 시간이 log2 (n)에 도달합니다.

1 2 3 .. 7 3 1 .... 8 4
4 5 6 + 2 4 3 => 6 9 9
7 8 9 .. 11 ... ... 8 9 9


3

크롤러와 같은 응용 프로그램에서 ForkJoin의 성능에 놀랄 것입니다. 여기 에서 배우는 가장 좋은 자습서가 있습니다.

포크 / 조인의 논리는 매우 간단합니다. (1) 각각의 큰 작업을 작은 작업으로 분리 (포크)합니다. (2) 각 작업을 별도의 스레드에서 처리합니다 (필요한 경우 더 작은 작업으로 분리). (3) 결과에 합류하십시오.


3

문제가 다른 스레드가 완료 될 때까지 기다려야하는 경우 (배열 정렬 또는 배열 합계의 경우와 같이) Executor (Executors.newFixedThreadPool (2))는 제한으로 인해 질식하므로 포크 조인을 사용해야합니다 스레드 수 이 경우 포크 조인 풀은 더 많은 스레드를 작성하여 차단 된 스레드가 동일한 병렬 처리를 유지하도록 덮습니다.

출처 : http://www.oracle.com/technetwork/articles/java/fork-join-422606.html

분할 가능 및 정복 알고리즘을 구현하기위한 실행 프로그램의 문제점은 서브 태스크 작성과 관련이 없습니다. Callable은 새 서브 태스크를 실행 프로그램에 자유롭게 제출하고 동기식 또는 비동기 방식으로 결과를 기다릴 수 있기 때문입니다. 문제는 병렬 처리의 문제입니다. Callable이 다른 Callable의 결과를 기다리는 경우 대기 상태가되므로 대기중인 다른 Callable을 처리 할 기회가 낭비됩니다.

Doug Lea의 노력을 통해 Java SE 7의 java.util.concurrent 패키지에 추가 된 fork / join 프레임 워크가 그 차이를 메 웁니다.

출처 : https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ForkJoinPool.html

풀은 일부 작업이 다른 작업에 참여하기 위해 대기중인 경우에도 내부 작업자 스레드를 동적으로 추가, 일시 중단 또는 다시 시작하여 충분한 활성 (또는 사용 가능한) 스레드를 유지하려고합니다. 그러나 차단 된 IO 또는 기타 관리되지 않는 동기화에 대해서는 그러한 조정이 보장되지 않습니다.

public int getPoolSize () 시작되었지만 아직 종료되지 않은 작업자 스레드 수를 반환합니다. 이 메소드에 의해 리턴 된 결과는 다른 스레드가 협력 적으로 차단 될 때 병렬 처리를 유지하기 위해 스레드가 작성 될 때 getParallelism ()과 다를 수 있습니다.


2

긴 답변을 읽을 시간이없는 사람들을 위해 짧은 답변을 추가하고 싶습니다. Applied Akka Patterns 책에서 비교를 수행합니다.

포크 조인 실행기 또는 스레드 풀 실행기를 사용할지 여부에 대한 결정은 주로 해당 디스패처의 작업이 차단되는지 여부에 따라 결정됩니다. 포크-조인 실행기는 최대 수의 활성 스레드를 제공하는 반면 스레드 풀 실행기는 고정 된 수의 스레드를 제공합니다. 스레드가 차단되면 포크 조인 실행기가 더 많이 생성하는 반면 스레드 풀 실행기는 생성하지 않습니다. 블로킹 작업의 경우 일반적으로 스레드 풀 실행기를 사용하는 것이 좋습니다. 스레드 수는 폭발하지 않기 때문입니다. 포크 조인 실행기에서 "반응"작업이 많을수록 좋습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.