[Java] JavaでFork / Join

ここに画像の説明を挿入

オリジナル:忘れた

フォーク/ジョインとは何ですか?

Fork / Joinフレームワークは、Java7によって提供される並列実行タスクフレームワークです。アイデアは、大きなタスクを小さなタスクに分解し、次に小さなタスクを分解し続けることができます。その後、各小さなタスクの結果が個別に計算されて結合され、最後に要約された結果が使用されます。大きなタスクの結果。この考え方はMapReduceの考え方と非常によく似ています。タスク分割の場合、各サブタスクは互いに独立している必要があり、互いに影響を与えずに独立して並列にタスクを実行できる必要があります。

Fork / Joinの操作フローチャートは以下のとおりです。

画像画像

このフレームワークは、Fork / Joinという単語の文字通りの意味で理解できます。forkはforkの意味です。つまり、大きなタスクは並列の小さなタスクに分解され、Joinは接続と結合の意味です。つまり、すべての並列の小さなタスクの実行結果が集約されます。

ここに画像の説明を挿入

ワークスチールアルゴリズム
ForkJoinは、ワークスチールアルゴリズムを使用します。ワーカースレッドのタスクキューが空で、実行するタスクがない場合、アクティブな実行のために他のワーカースレッドからタスクを取得します。ワークスティーリングを実現するために、ワーカースレッドで両方向キューが維持され、スティーリングタスクスレッドがキューの最後からタスクを取得し、盗まれたタスクスレッドがキューの先頭からタスクを取得します。このメカニズムは、並列計算のためにスレッドを最大限に活用し、スレッドの競合を減らします。ただし、キューにタスクが1つしかない場合は、2つのスレッドでフェッチすると、リソースが浪費されます。

ワークスチールの操作フローチャートは以下のとおりです。

ここに画像の説明を挿入

Fork / Joinコアクラス
Fork / Joinフレームワークは、主にサブタスクとタスクスケジューリングの2つの部分で構成され、クラス階層図は次のとおりです。

画像
画像

ForkJoinPool

ForkJoinPoolは、ForkJoinフレームワークのタスクスケジューラで、ThreadPoolExecutorのような独自のスレッドプールを実装し、サブタスクをスケジュールするための3つのメソッドを提供します。

execute:指定されたタスクを非同期で実行し、結果は返されません;
invoke、invokeAll:指定されたタスクを
非同期で実行し、完了を待って結果返します; submit:指定されたタスクを非同期で実行し、Futureオブジェクトをすぐに返します;
ForkJoinTask
Fork / Joinフレームワークでの実際の実行タスククラスには以下の2つの実装があり、通常これら2つの実装クラスは継承可能です。

RecursiveAction:結果が返されないサブタスクに使用されます;
RecursiveTask:結果が返されたサブタスクに使用されます。

フォーク/ジョインフレームワークの戦闘

以下は、Fork / Joinの小さな例です。1+ 2 +…10億から、各タスクは1000の数値のみを処理して追加できます。1000を超える数は、並列処理のために小さなタスクに自動的に分解されます。参加と使用の時間消費の比較。

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;

public class ForkJoinTask extends RecursiveTask<Long> {
    
    

	private static final long MAX = 1000000000L;
	private static final long THRESHOLD = 1000L;
	private long start;
	private long end;

	public ForkJoinTask(long start, long end) {
    
    
		this.start = start;
		this.end = end;
	}

	public static void main(String[] args) {
    
    
		test();
		System.out.println("--------------------");
		testForkJoin();
	}

	private static void test() {
    
    
		System.out.println("test");
		long start = System.currentTimeMillis();
		Long sum = 0L;
		for (long i = 0L; i <= MAX; i++) {
    
    
			sum += i;
		}
		System.out.println(sum);
		System.out.println(System.currentTimeMillis() - start + "ms");
	}

	private static void testForkJoin() {
    
    
		System.out.println("testForkJoin");
		long start = System.currentTimeMillis();
		ForkJoinPool forkJoinPool = new ForkJoinPool();
		Long sum = forkJoinPool.invoke(new ForkJoinTask(1, MAX));
		System.out.println(sum);
		System.out.println(System.currentTimeMillis() - start + "ms");
	}

	@Override
	protected Long compute() {
    
    
		long sum = 0;
		if (end - start <= THRESHOLD) {
    
    
			for (long i = start; i <= end; i++) {
    
    
				sum += i;
			}
			return sum;
		} else {
    
    
			long mid = (start + end) / 2;

			ForkJoinTask task1 = new ForkJoinTask(start, mid);
			task1.fork();

			ForkJoinTask task2 = new ForkJoinTask(mid + 1, end);
			task2.fork();

			return task1.join() + task2.join();
		}
	}

}

ここでは計算結果が必要なので、タスクはRecursiveTaskクラスを継承します。ForkJoinTaskは、computeメソッドを実装する必要があります。このメソッドでは、最初に、タスクがしきい値1000以下であるかどうかを判別し、そうである場合は、タスクを直接実行する必要があります。それ以外の場合は、2つのサブタスクに分割されます。各サブタスクがforkメソッドを呼び出すと、computeメソッドに入り、現在のサブタスクを孫タスクに引き続き分割する必要があるかどうかを確認します。分割を続行する必要がない場合は、現在のサブタスクが実行され、結果が返されます。joinメソッドを使用すると、サブタスクがブロックされて完了し、その結果が得られるまで待機します。

プログラム出力:

test
500000000500000000
4992ms
--------------------
testForkJoin
500000000500000000
508ms

結果から、並列の時間消費は、直列の時間消費よりも大幅に少ないことがわかります。これは、並列タスクの利点です。

それにもかかわらず、Fork / Joinを使用するときは注意が必要です。盲目的に使用しないでください。

タスクが深く逆アセンブルされると、システム内のスレッド数が累積し、システムパフォーマンスが大幅に低下します。
関数呼び出しスタックが深い場合は、スタックメモリがオーバーフローします。

おすすめ

転載: blog.csdn.net/qq_21383435/article/details/108498305