[동시 프로그래밍] ForkJoinPool 작동 원리 분석

앞 내용

Q1: 동시 프로그래밍에서는 일반적으로 어떤 유형의 작업을 접하게 됩니까?
답변: 일반적으로 컴퓨팅 집약적(CPU 집약적), IO 집약적입니다.

Q2: 그들 사이의 차이점은 무엇입니까?
답변:계산 집약적인 작업은 CPU 리소스를 소비하는 많은 계산이 특징입니다.pi 계산, 고화질 비디오 디코딩 등과 같은 모든 작업은 CPU의 컴퓨팅 성능에 의존합니다.이러한 종류의 컴퓨팅 집약적인 작업은 멀티태스킹으로도 완료할 수 있지만 작업이 많을수록 작업 전환에 더 많은 시간이 소요되고 CPU 실행 작업의 효율성이 낮아집니다.따라서 CPU를 가장 효율적으로 사용하려면 동시에 수행되는 계산 집약적인 작업의 수가 CPU의 코어 수와 같아야 합니다.
컴퓨팅 집약적인 작업은 주로 CPU 리소스를 소비하므로 코드 실행의 효율성이 매우 중요합니다. Python과 같은 스크립팅 언어는 매우 비효율적으로 실행되며 계산 집약적인 작업에는 전혀 적합하지 않습니다. 계산 집약적인 작업의 경우 C로 작성하는 것이 가장 좋습니다.
IO 집약적입니다. 네트워크 및 디스크 IO와 관련된 작업은 모두 IO 집약적 작업입니다. 이 유형의 작업은 CPU 소비가 낮고 작업의 대부분의 시간이 IO 작업이 완료될 때까지 기다리는 것이 특징입니다.(IO의 속도는 CPU와 메모리의 속도보다 훨씬 느리기 때문입니다.)IO 집약적인 작업의 경우 작업이 많을수록 CPU 효율성은 높아지지만 한계가 있습니다.. 가장 일반적인 작업은 웹 애플리케이션과 같은 IO 집약적인 작업입니다.
IO 집약적인 작업을 실행하는 동안 99%의 시간이 IO에 소요되고 CPU에 소요되는 시간은 거의 없으므로 Python과 같은 스크립트 언어를 매우 빠르게 실행되는 C 언어로 대체하는 것은 전혀 불가능합니다. .운영 효율성을 향상시킵니다. IO 집약적인 작업의 경우 개발 효율성이 가장 높은(코드량이 가장 적은) 언어가 가장 적합한 언어이며, 스크립팅 언어가 첫 번째 선택이고, C 언어가 최악입니다.

코스 내용

1. 알고리즘 문제로 인해 촉발된 생각

1. 알고리즘 질문

[멀티코어 CPU의 성능을 최대한 활용하여 2천만 크기의 배열을 빠르게 정렬하는 방법은 무엇입니까?]라는 알고리즘 질문이 있습니다. ]
정렬 알고리즘에 관해서는 다들 어느 정도 감동을 받을 거라 생각하는데, 결국 많은 인터뷰에서 가끔씩 질문을 하게 될 것입니다. 예를 들면 버블 정렬, 선택 정렬, 퀵 정렬 등이 있습니다. 하지만 이 2KW 용량에서는 분명히 적용할 수 없습니다. 아마도 경험이 많은 친구들은 병합 정렬 방법을 생각했을 것입니다.
네, 제가 소개하고 싶은 것이 [병합 정렬 방법]입니다.

2. 병합정렬이란?

병합 정렬(Merge Sort)은 분할 정복을 기반으로 하는 정렬 알고리즘입니다. 병합 정렬의 기본 아이디어는 큰 배열을
동일한 크기의 두 개의 하위 배열로 나누고 각 하위 배열을 개별적으로 정렬한 다음 두 개의 하위 배열을 큰 정렬 배열로 병합하는 것입니다.
재귀적 구현이 자주 사용되기 때문에(분할 및 병합의 특성에 따라 결정됨) 이를 병합 정렬이라고 부릅니다.
병합 정렬 단계에는 다음 세 단계가 포함됩니다.

  • 배열을 두 개의 하위 배열로 분할
  • 각 하위 배열 정렬
  • 두 개의 정렬된 하위 배열 병합

병합 정렬의 시간 복잡도는 O(nlogn)이고 공간 복잡도는 O(n)입니다. 여기서 n은 배열의 길이입니다.
물론 좀 더 학문적인 설명도 있습니다.

분할 정복의 개념은 N 크기의 문제를 K개의 더 작은 하위 문제로 분해하는 것입니다.이러한 하위 문제는 서로 독립적이며 원래 문제와
동일한 성격을 갖습니다.
. 원래 문제의 해는 하위 문제의 해를 구함으로써 얻을 수 있습니다.
사고를 분할하고 정복하는 단계는 다음과 같습니다.

  1. 분해(Decomposition): 해결해야 할 문제를 동일한 유형의 여러 개의 작은 문제로 나눕니다.
  2. 해결: 하위 문제가 충분히 작은 크기로 분할되면 더 간단한 방법을 사용하여 문제를 해결합니다.
  3. 병합: 원래 문제의 요구 사항에 따라 하위 문제의 솔루션을 계층별로 결합하여 원래 문제의 솔루션을 형성합니다.
    10가지 고전적인 컴퓨터 알고리즘 중 병합 정렬, 퀵 정렬, 이진 검색은 모두 분할 정복 아이디어를 기반으로 구현된 알고리즘입니다.
    분할 정복 작업 모델 다이어그램은 다음과 같습니다.

여기서는 이 알고리즘 문제를 간단히 구현하기 위해 병합정렬 알고리즘을 사용하였으며, 코드는 다음과 같다.

public class MergeSort {
    
    

    private final int[] arrayToSort; //要排序的数组
    private final int threshold;  //拆分的阈值,低于此阈值就不再进行拆分

    public MergeSort(final int[] arrayToSort, final int threshold) {
    
    
        this.arrayToSort = arrayToSort;
        this.threshold = threshold;
    }

    /**
     * 排序
     * @return
     */
    public int[] sequentialSort() {
    
    
        return sequentialSort(arrayToSort, threshold);
    }

    public static int[] sequentialSort(final int[] arrayToSort, int threshold) {
    
    
        //拆分后的数组长度小于阈值,直接进行排序
        if (arrayToSort.length < threshold) {
    
    
            //调用jdk提供的排序方法
            Arrays.sort(arrayToSort);
            return arrayToSort;
        }

        int midpoint = arrayToSort.length / 2;
        //对数组进行拆分
        int[] leftArray = Arrays.copyOfRange(arrayToSort, 0, midpoint);
        int[] rightArray = Arrays.copyOfRange(arrayToSort, midpoint, arrayToSort.length);
        //递归调用
        leftArray = sequentialSort(leftArray, threshold);
        rightArray = sequentialSort(rightArray, threshold);
        //合并排序结果
        return merge(leftArray, rightArray);
    }

    public static int[] merge(final int[] leftArray, final int[] rightArray) {
    
    
        //定义用于合并结果的数组
        int[] mergedArray = new int[leftArray.length + rightArray.length];
        int mergedArrayPos = 0;
        int leftArrayPos = 0;
        int rightArrayPos = 0;
        while (leftArrayPos < leftArray.length && rightArrayPos < rightArray.length) {
    
    
            if (leftArray[leftArrayPos] <= rightArray[rightArrayPos]) {
    
    
                mergedArray[mergedArrayPos] = leftArray[leftArrayPos];
                leftArrayPos++;
            } else {
    
    
                mergedArray[mergedArrayPos] = rightArray[rightArrayPos];
                rightArrayPos++;
            }
            mergedArrayPos++;
        }

        while (leftArrayPos < leftArray.length) {
    
    
            mergedArray[mergedArrayPos] = leftArray[leftArrayPos];
            leftArrayPos++;
            mergedArrayPos++;
        }

        while (rightArrayPos < rightArray.length) {
    
    
            mergedArray[mergedArrayPos] = rightArray[rightArrayPos];
            rightArrayPos++;
            mergedArrayPos++;
        }

        return mergedArray;
    }

    public static void main(String[] args) {
    
    

        // 初始化一个2KW的数组
        Random random = new Random();
        int[] arrays = new int[20000000];
        for (int i = 0; i < 20000000; i++) {
    
    
            arrays[i] = random.nextInt(5);
        }


        long start1 = System.currentTimeMillis();

        // 开始拆分排序
        MergeSort mergeSort = new MergeSort(arrays, 100000);
        mergeSort.sequentialSort();
        System.out.println("任务总耗时:wasteTime=" + (System.currentTimeMillis() - start1));
    }
//    系统输出:
//    任务总耗时:wasteTime=921
} 

총 시간 소모는 921ms임을 알 수 있습니다.(이것은 여전히 ​​내 컴퓨터에서는 상대적으로 좋은 수준입니다.)
여기서는 921ms가 좋은 것 같다는 것을 모두가 알 수 있을 것이라고 생각하는데, 데이터 규모가 더 크다면 어떨까요? 그때는 단순한 축적이 아니라 기하급수적인 성장이 될 수도 있다는 점을 책임감 있게 말씀드릴 수 있습니다. 그리고 신중한 친구들은 위의 코드가 단일 스레드 환경에서 완전히 실행되고 있음을 이미 보았을 것입니다(한 코어는 완전히 로드되고 다른 코어는 몇 년 동안 조용하고 견딜 수 없음). , 더 잘 작동할까요?
우리는 어떤 최적화를 하든 사실 CPU 자원을 최대한 짜내는 것이 많은 목적이라는 것을 알고 있습니다. , 이 행동은 자원 낭비입니다(/dog head/dog head). 아래 그림과 같이 CPU 사용률 13%, 자본가들이 보면 한숨이 나옵니다. 정말 낭비입니다!
여기에 이미지 설명을 삽입하세요
따라서 위에서 언급한 [하나의 코어는 앞으로 로드되고 다른 코어는 시간이 지나면 조용해집니다]를 해결하고 [병합 알고리즘]에 적응하기 위해 새로운 스레드 풀이 제안됩니다 ForkJoinPool.
나는 모든 사람들이 질문을 가질 것이라고 믿습니다: 스레드 풀? 그렇다면 이전 ThreadPoolExecutor를 사용하면 어떨까요?정말 효과가 없다고 밖에 말할 수 없다! 왜?
JVM에 대해 어느 정도 이해하고 있다면 함수 호출 깊이가 +1될 때마다 스택 프레임 수가 +1된다는 것을 알아야 합니다. (관심 있는 학생은 이전 기사 [JVM 특별 주제] JVM 메모리 모델 심층 분석 및 최적화 ) 이므로 이 [병합(재귀) 알고리즘]에서는 분할의 세분성이 미세해질수록 스택 프레임 수가 증가하여 결국 결과가 발생합니다 StackOverFlowError. 또는 이렇게 표현하면 일반 스레드 풀에서 병합 알고리즘을 구현하려면 반환 값을 기다려야 하므로 메인 스레드가 차단되고 하위 스레드의 실행 결과를 기다리게 됩니다. 작업을 수만 개로 나눈다면, 수십만, 수백만 개의 작업이라도 여전히 수만, 심지어는 수십만, 수백만 개의 스레드를 열어야 할 가능성이 있을까요? ? 생각해봐도 기존 컴퓨터로는 제공하기가 불가능합니다(위 그림을 보시면 제 컴퓨터는 정상 작동시 스레드가 4000개 이상밖에 안 됩니다).

2. Fork/Join 프레임워크란 무엇입니까?

1. 기본 소개

Fork/Join 프레임워크는 작업을 병렬로 실행하기 위해 Java7에서 제공하는 프레임워크로, 큰 작업을 여러 개의 작은 작업으로 나누고, 최종적으로 각 작은 작업의 결과를 요약하여 큰 작업의 결과를 얻기 위한 프레임워크입니다(나누기) 그리고 생각을 정복하라). (Fork는 칼과 포크의 의미; Join은 연결의 의미. Fork를 사용하여 무언가를 분리한 후 최종적으로 다시 연결함) Fork는
큰 작업을 여러 개의 하위 작업으로 나누어 병렬 실행하는 것이고, Join은 병합하는 것 이러한 하위 작업은 실행 결과이고, 마지막으로 이 큰 작업의 결과를 얻습니다. 예를 들어, 컴퓨팅은 1+2+ ...+2000000010개의 하위 작업으로 나눌 수 있으며, n개의 하위 작업 각각은 2,000,000개의 숫자를 합산하고 마지막으로 이 10개의 하위 작업의 결과를 요약합니다. 아래 그림과 같이
여기에 이미지 설명을 삽입하세요
다음과 같은 일반적인 사용 시나리오가 있습니다.

  1. 재귀적 분해 작업
    Fork/Join 프레임워크는 정렬, 병합, 순회와 같은 재귀적 분해 작업에 특히 적합합니다. 이러한 작업은 일반적으로 큰 작업을 여러 개의 하위 작업으로 분해할 수 있으며, 각 하위 작업은 독립적으로 실행될 수 있으며, 하위 작업의 결과는 병합 작업을 통해 순서가 지정된 결과로 결합될 수 있습니다.
  2. 배열 처리
    Fork/Join 프레임워크는 배열 정렬, 검색 및 통계와 같은 배열 처리에도 사용할 수 있습니다. 대규모 배열을 처리할 때 Fork/Join 프레임워크는 배열을 여러 하위 배열로 나누고 각 하위 배열을 병렬로 처리한 다음 마지막으로 처리된 하위 배열을 정렬된 큰 배열로 병합할 수 있습니다.
  3. 병렬 알고리즘
    Fork/Join 프레임워크는 병렬 이미지 처리 알고리즘 및 병렬 기계 학습 알고리즘과 같은 병렬 알고리즘을 구현하는 데에도 사용할 수 있습니다. 이러한 알고리즘에서는 문제를 여러 하위 문제로 분해하고 각 하위 문제를 병렬로 해결한 다음 하위 문제의 결과를 결합하여 최종 솔루션을 얻을 수 있습니다.
  4. 빅데이터 처리
    Fork/Join 프레임워크는 대용량 로그 파일 처리, 대용량 데이터베이스 쿼리 등 빅데이터 처리에도 사용할 수 있습니다. 대용량 데이터를 처리할 때 데이터를 여러 개의 조각으로 나눌 수 있으며, 각 조각은 병렬로 처리되고, 마지막으로 처리된 조각이 병합되어 완전한 결과가 됩니다.

2.ForkJoinPool

ForkJoinPool은 Fork/Join 프레임워크의 스레드 풀 클래스로, Fork/Join 작업을 위한 스레드를 관리하는 데 사용됩니다. 마찬가지로 클래스 ThreadPoolExecutor에서도 상속되므로 작업 제출, 작업 실행, 스레드 풀 닫기 및 대기에 대해 submit(), Invoke(), shutdown(), waitTermination() 등과 동일한 동작을 갖습니다. 태스크 실행 결과. ForkJoinPool 클래스에는 스레드 풀의 크기, 작업자 스레드의 우선 순위, 작업 대기열의 용량 등과 같은 일부 매개 변수도 포함되어 있으며 특정 응용 프로그램 시나리오에 따라 설정할 수 있습니다. 클래스 다이어그램은 다음과 같습니다.AbstractExecutorServiceThreadPoolExecutor
여기에 이미지 설명을 삽입하세요

여기에는 다음과 같은 속성이 있습니다.

  1. ForkJoinPool은 ExecutorService를 대체하기 위한 것이 아니라 이를 보완하는 것입니다., 일부 애플리케이션 시나리오에서는 ExecutorService보다 성능이 더 좋습니다.
  2. ForkJoinPool은 주로 "분할 및 정복" 알고리즘, 특히 퀵 정렬 등과 ​​같이 분할 및 정복 후에 재귀적으로 호출되는 함수를 구현하는 데 사용됩니다.
  3. ForkJoinPool은 컴퓨팅 집약적인 작업에 가장 적합하며, I/O, 스레드 간 동기화, sleep() 등으로 스레드를 오랫동안 차단할 수 있는 경우 ManagedBlocker를 사용하는 것이 가장 좋습니다.

위에서 언급했듯이 ForkJoinPool은 ThreadPoolExecutor를 보완한 것으로 전자가 컴퓨팅 집약적인 작업에 적합하고 후자가 일반적으로 IO 집약적인 작업에 적합합니다.

2. ForkJoinPool 생성자와 매개변수의 해석

여기에 이미지 설명을 삽입하세요

public ForkJoinPool(int parallelism,
                        ForkJoinWorkerThreadFactory factory,
                        UncaughtExceptionHandler handler,
                        boolean asyncMode)

위 코드에서 볼 수 있듯이 ForkJoinPool에는 4개의 핵심 매개변수가 있는데, 이는 스레드 풀의 병렬 스레드 수 제어, 작업자 스레드 생성, 예외 처리, 대기열 모드 지정에 사용됩니다. 매개변수는 다음과 같이 설명됩니다.

  1. int parallelism: 병렬 처리 수준을 지정합니다. ForkJoinPool은 이 설정에 따라 작업자 스레드 수를 결정합니다. 설정되지 않은 경우 병렬 처리 수준은 Runtime.getRuntime().availableProcessors()를 사용하여 설정됩니다.
  2. ForkJoinWorkerThreadFactory factory: ForkJoinPool이 스레드를 생성하면 팩토리를 통해 생성됩니다. 여기서 구현해야 할 것은 ThreadFactory가 아닌 ForkJoinWorkerThreadFactory입니다. 팩토리를 지정하지 않으면 기본 DefaultForkJoinWorkerThreadFactory가 스레드 생성을 담당합니다.
  3. UncaughtExceptionHandler handler: 예외 처리기를 지정합니다. 작업 실행 중에 오류가 발생하면 설정된 처리기에 의해 처리됩니다.
  4. boolean asyncMode: 대기열의 작업 모드를 설정합니다. asyncMode가 true이면 선입선출(FIFO) 큐가 사용되고, false이면 후입선출(Last In First Out) 모드가 사용됩니다.

3. 과제 제출 방법

작업 제출은 ForkJoinPool의 핵심 기능 중 하나이며 작업을 제출하는 방법에는 세 가지가 있습니다.
여기에 이미지 설명을 삽입하세요

4. 작동 원리 다이어그램

아래 그림을 기억하세요, ThreadPoolExecutor의 작동 원리 다이어그램:
여기에 이미지 설명을 삽입하세요
ForkJoinPool은 ThreadPoolExecutor와 다릅니다. 더 큰 병렬성에 적응하기 위해 작업 대기열의 디자인을 수정했습니다. 그 개략도는 다음과 같습니다:
여기에 이미지 설명을 삽입하세요

ForkJoinPool 내부에는 여러 작업 대기열이 있습니다. ForkJoinPool의 Invoke() 또는 submit() 메서드를 통해 작업을 제출하면 ForkJoinPool은 특정 라우팅 규칙에 따라 작업을 작업 대기열에 제출합니다. task, 그러면 하위 작업이 작업 대기열에 제출됩니다. 작업자 스레드에 해당합니다( 위 그림의 Task-4와 같이 Task-4.1과 Task-4.2는 포크 방식을 통해 더욱 분리됩니다 ).
작업자 스레드에 해당하는 작업 대기열이 비어 있으면 수행할 작업이 없는 것인가요? 아니요,ForkJoinPool은 [작업 도용]이라는 메커니즘을 지원합니다. 작업자 스레드가 유휴 상태인 경우 다른 작업자 작업 대기열의 작업을 "훔칠" 수 있습니다.. 이런 방식으로 모든 작업자 스레드는 유휴 상태가 되지 않습니다.

5. 일자리 도둑질

ForkJoinPool과 ThreadPoolExecutor의 큰 차이점은 ForkJoinPool의 존재로 인해 성능 보장의 핵심 중 하나인 작업 도용 설계가 도입된다는 점입니다. 작업 도용은 유휴 스레드가 사용 중인 스레드의 양방향 대기열에서 작업을 도용할 수 있도록 허용하는 것입니다. 기본적으로 작업자 스레드는 자체 deque의 헤드에서 작업을 가져옵니다. 그러나 자체 작업이 비어 있으면 스레드는 다른 사용 중인 스레드의 deque 꼬리에서 작업을 가져옵니다. 이 접근 방식은 스레드가 작업을 위해 경쟁할 가능성을 최소화합니다.

ForkJoinPool 작업의 대부분은 내부 클래스 WorkQueue에 의해 구현되는 작업 도용 대기열에서 발생합니다. 이는 Deques의 특별한 형태이지만 push, pop 및 poll(stealing이라고도 함)의 세 가지 작업만 지원합니다. ForkJoinPool에서는 큐 읽기에 엄격한 제한이 있는데, push와 pop은 자신이 속한 스레드에서만 호출할 수 있고, poll은 다른 스레드에서 호출할 수 있습니다.
작업 도용을 통해 Fork/Join 프레임워크는 다음을 수행할 수 있습니다.멀티 코어 CPU의 컴퓨팅 성능을 최대한 활용하고 동시에 스레드 부족 및 지연 문제를 방지하기 위해 작업의 자동 로드 밸런싱을 실현합니다.
여기에 이미지 설명을 삽입하세요

6. 공통 스레드 풀과 일반 스레드 풀의 차이점

  • 업무 훔치기 알고리즘
    ForkJoinPool은 다중 작업 대기열과 작업 도용 알고리즘을 사용하여 스레드 활용도를 향상시킵니다., 일반 스레드 풀은 공유 차단 작업 대기열을 사용하여 작업을 관리합니다. 작업 도용 알고리즘에서는 스레드가 자신의 작업을 완료하면 다른 스레드의 큐에서 실행할 작업을 가져와 스레드의 활용률을 향상시킬 수 있습니다.
  • 작업 분해 및 병합
    ForkJoinPool은 큰 작업을 여러 개의 작은 작업으로 분해하고, 이러한 작은 작업을 병렬로 실행하고, 마지막으로 결과를 결합하여 최종 결과를 얻을 수 있습니다. 일반 스레드 풀은 제출된 작업 순서대로 하나씩만 작업을 실행할 수 있습니다.
  • 작업자 스레드 수
    ForkJoinPool은 CPU의 성능 이점을 극대화하기 위해 현재 시스템의 CPU 코어 수에 따라 작업자 스레드 수를 자동으로 설정합니다. 일반 스레드 풀은 스레드 풀의 크기를 수동으로 설정해야 하며, 설정이 무리한 경우 스레드가 너무 많거나 적어 프로그램 성능에 영향을 줄 수 있습니다.
  • 작업 유형
    ForkJoinPool은 대규모 작업 병렬화를 수행하는 데 적합합니다., 일반 스레드 풀은 요청 처리와 같은 일부 짧은 작업을 수행하는 데 적합합니다.

7.ForkJoinTask

ForkJoinTask는 작업 실행을 위한 기본 인터페이스를 정의하는 Fork/Join 프레임워크의 추상 클래스입니다. 사용자는 ForkJoinTask 클래스를 상속하여 자신만의 작업 클래스를 구현하고, Compute() 메서드를 다시 작성하여 작업의 실행 논리를 정의할 수 있습니다. 일반적으로 ForkJoinTask 클래스를 직접 상속할 필요는 없지만 해당 하위 클래스만 상속하면 됩니다. Fork/Join 프레임워크는 다음 세 가지 하위 클래스를 제공합니다.

RecursiveAction: 재귀적으로 실행되지만 결과를 반환할 필요가 없는 작업용입니다.
RecursiveTask : 결과를 반환해야 하는 작업을 반복적으로 실행하는 데 사용됩니다.
CountedCompleter: 작업이 완료된 후 실행되도록 사용자 지정 후크 함수가 트리거됩니다.

ForkJoinTask의 핵심은 작업 제출과 결과 획득을 위한 주요 작업 조정 기능을 수행하는 fork() 메서드와 Join() 메서드입니다.

fork()——작업 제출
fork() 메서드는 현재 작업이 실행 중인 스레드 풀에 작업을 제출하는 데 사용됩니다. 현재 스레드가 ForkJoinWorkerThread 유형이면 스레드의 작업 대기열에 들어가고, 그렇지 않으면 공통 스레드 풀의 작업 대기열에 넣습니다.

Join() ——작업 실행 결과 가져오기
Join() 메소드는 작업 실행 결과를 가져오는 데 사용됩니다. Join()을 호출하면 해당 하위 작업의 실행이 완료되고 결과가 반환될 때까지 현재 스레드가 차단됩니다.

요약하다

  1. ForkJoinPool과 그 원리, 작업 대기열 설계를 학습했습니다.
  2. ForkJoinPool과 ThreadPoolExecutor의 차이점을 배웠습니다.

추천

출처blog.csdn.net/qq_32681589/article/details/132000844