Fork/Join 框架

低吟不作語發表於2021-03-27

本文部分摘自《Java 併發程式設計的藝術》


Fork/Join 框架概述

Fork/Join 框架是 Java7 提供的一個用於並行執行任務的框架,是把一個大任務分割成若干個小任務,最終彙總每個小任務結果後得到大任務結果的框架,其執行流程如圖所示:


工作竊取演算法

工作竊取演算法是指某個執行緒從其他佇列裡竊取任務來執行,為什麼要這樣做呢?假如我們需要做一個比較大的任務,可以把這個任務分割為若干個互不依賴的子任務,為了減少執行緒間的競爭,把這些子任務分別放到不同的佇列裡,併為每個佇列建立一個單獨的執行緒來執行佇列裡的任務,執行緒和佇列一一對應。然而,如果某一執行緒先把自己佇列的任務幹完了,而其他執行緒對應的佇列裡還有任務等待處理,幹完活的執行緒與其等著,不如去幫其他執行緒幹活,這就是工作竊取演算法的動機。

為了減少竊取任務執行緒和被竊取任務執行緒之間的競爭,通常會使用雙端佇列,被竊取任務執行緒永遠從佇列的頭部拿任務執行,而竊取任務的執行緒永遠從雙端佇列的尾部拿任務執行


使用 Fork/Join 框架

首先思考一下,如果讓我們來設計一個 Fork/Join 框架,該如何設計呢?

  1. 分割任務

    首先我們需要一個有 fork 類來把大任務分割成子任務,有可能子任務還是很大,所以需要不停地分割,直到分割出來的子任務足夠小

  2. 執行任務併合並結果

    分割的子任務分別放在雙端佇列裡,然後幾個啟動執行緒分別從雙端佇列裡獲取任務執行。子任務執行完的結果都統一放在一個佇列裡,啟動一個執行緒從佇列裡拿資料,然後進行合併

Fork/Join 使用兩個類來完成以上兩件事情:

  1. ForkJoinTask

    我們使用 ForkJoin 框架,必須首先建立 ForkJoin 任務,它提供在任務中執行 fork() 和 join() 操作的機制。通常情況下,我們不需要直接繼承 ForkJoinTask 類,只需要繼承它的子類即可,Fork/Join 框架提供了以下兩個子類:

    • RecursiveAction:用於沒有返回結果的任務
    • RecursiveTask:用於有返回結果待任務
  2. ForkJoinPool

    ForkJoinTask 需要通過 ForkJoinPool 來執行

我們通過一個簡單的需求來使用 Fork/Join 框架,需求是:計算 1+2+3+4 的結果

使用 Fork/Join 框架把這個任務 fork 成兩個子任務,子任務一負責計算 1+2,子任務而負責計算 3+4,然後再 join 兩個子任務的結果,因為是有結果的任務,所以必須繼承 RecursiveTask,程式碼實現如下:

public class CountTask extends RecursiveTask<Integer> {

    // 閾值
    private static final int THRESHOLD = 2;

    private final int start;
    private final int end;

    public CountTask(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        int sum = 0;
        boolean canCompute = (end - start) <= THRESHOLD;
        // 如果任務足夠小就計算任務
        if (canCompute) {
            for (int i = start; i <= end; i++) {
                sum += i;
            }
        } else {
            // 如果任務大於閾值,就分裂成兩個子任務計算
            int middle = (start + end) / 2;
            CountTask leftTask = new CountTask(start, middle);
            CountTask rightTask = new CountTask(middle + 1, end);
            // 執行子任務
            leftTask.fork();
            rightTask.fork();
            // 等待子任務執行完,並得到其結果
            int leftResult = leftTask.join();
            int rightResult = rightTask.join();
            // 合併子任務
            sum = leftResult + rightResult;
        }
        return sum;
    }

    public static void main(String[] args) {
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        // 生成一個計算任務,負責計算 1+2+3+4
        CountTask task = new CountTask(1, 4);
        // 執行一個任務
        Future<Integer> result = forkJoinPool.submit(task);
        try {
            System.out.println(result.get());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

上面的例子中是通過 new ForkJoinPool(),然而這並不是其作者 Doug Lea 推薦的方式。ForkJoinPool 類有一個靜態方法commonPool(),它所獲得的 ForkJoinPool 例項是由整個應用程式共享的,可以幫助應用程式中多個需要進行歸併計算的任務共享計算資源

ForkJoinPool forkJoinPool = ForkJoinPool.commonPool();

ForkJoinTask 在執行的時候可能會丟擲異常,但我們沒辦法在主執行緒直接捕獲執行緒,所以 ForkJoinTask 提供了 isCompletedAbnormally() 方法來檢查任務是否已經丟擲異常或已經被取消,並可以通過 ForkJoinTask 的 getException 方法獲取異常

if(task.isCompletedAbnormally()) {
    System.out.println(task.getException());
}

Fork/Join 框架的實現原理

ForkJoinPool 中用來處理任務的工作執行緒採用的是 ForkJoinWorkerThread,它繼承了 Thread 類,擁有兩個非常關鍵的變數

final ForkJoinPool pool;
final ForkJoinPool.WorkQueue workQueue;

pool 是這個工作執行緒所屬的 ForkJoinPool 例項,workQueue 是一個雙端佇列,可以發現,它是 ForkJoinPool 的一個內部類,其結構如下(省略部分程式碼)

static final class WorkQueue {
	...    
    ForkJoinTask<?>[] array;
    final ForkJoinPool pool;
    final ForkJoinWorkerThread owner;
	...
}

WorkQueue 裡維護一個 ForkJoinTask 陣列,用來存放待執行的任務(ForkJoinTask)。所以 Fork/Join 框架的基本思想就是:ForkJoinPool 的每個工作執行緒都維護著一個工作佇列(WorkQueue),裡面存放的物件是任務,每個工作執行緒處理自己的工作佇列裡的任務

fork() 方法做的工作只有一件事,既是把任務推入當前工作執行緒的工作佇列裡

public final ForkJoinTask<V> fork() {
    Thread t;
    if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread)
        ((ForkJoinWorkerThread)t).workQueue.push(this);
    else
        ForkJoinPool.common.externalPush(this);
    return this;
}

join() 方法的工作則複雜一些,首先會判斷執行緒是否為 ForkJoinThread 執行緒,如果不是,阻塞當前執行緒,等待任務完成,如果是,則不阻塞。接著檢視任務的完成狀態,如果已經完成,直接返回結果,否則從佇列中取出任務執行


相關文章