1. ホーム
  2. java

[解決済み] fork/joinフレームワークはスレッドプールより優れているか?

2022-05-10 11:25:11

質問

新しい fork/join フレームワーク を使う利点は何でしょうか。単純に大きなタスクを N 個のサブタスクに分割し、それらをキャッシュされたスレッドプール ( 実行者 から)キャッシュされたスレッドプールに送信し、各タスクが完了するのを待つという方法よりも優れているのでしょうか?フォーク/結合の抽象化を使用することで、問題がどのように単純化されるか、またはソリューションが何年も前からあるものより効率的になるのか、私にはわかりません。

たとえば、並列化されたブラーリング アルゴリズムは チュートリアルの例 はこのように実装することができます。

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)スレッドがある限り、すべてのプロセッサは計算全体が終了するまで全速力で働きます。

私は何かを見逃しているのでしょうか?fork/join フレームワークを使用することの付加価値は何でしょうか?

どのように解決するのですか?

基本的な誤解は、Fork/Joinの例では、以下のようになります。 ではない 動作を示す 盗む という、ある種の標準的な分割統治に過ぎない。

仕事の盗みとは、このようなものです。労働者Bは自分の仕事を終えました。彼は親切な人なので、周りを見渡すと、労働者Aがまだ一生懸命働いているのを見つけました。彼は散策して尋ねます: "ヘイ、若者よ、私はあなたに手を貸すことができます,quot;クール、私は1000ユニットのこのタスクを持っています。これまでのところ、私は655を残して345を終了しました。Bは「よし、早くパブに行けるように始めよう」と言いました。

このように、作業者は実際の作業を開始しても、お互いにコミュニケーションを取らなければなりません。これが例の中で欠けている部分です。

一方、例では、「下請け業者を使う」というようなことしか書かれていません。

作業員A: "くっ、1000単位の仕事がある。俺には多すぎる。この作業は、大きなタスクが10ユニットずつの小さなパケットに分割されるまで続けられます。これが、大きな仕事が10個ずつの小さなパケットに分解され、それを空いている作業者が実行する。しかし、あるパケットが毒薬のようなもので、他のパケットよりもかなり時間がかかるとしたら -- 運が悪いことに、分割フェーズは終了です。

Fork/Joinとタスクの先行分割の間に残る唯一の違いは、これです。前もって分割する場合、ワーク キューは最初からいっぱいです。例:1000個、閾値は10なので、キューには100個のエントリーがあります。これらのパケットはスレッドプールメンバーに配布されます。

Fork/Joinはより複雑で、キュー内のパケット数をより少なく保とうとするものです。

  • ステップ1: (1...1000)を含む1つのパケットをキューに入れる
  • ステップ2: 一人のワーカーがパケット(1...1000)をポップして、二つのパケットに置き換えます。(1...500)と(501...1000)の2つのパケットに置き換えます。
  • ステップ3: 1人のワーカーがパケット(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) に分割されます。 ....

Fork/Joinではキューが小さく(例では6)、quot;split"とquot;work"のフェーズはインターリーブされていることが分かります。

複数のワーカーが同時にポッピングとプッシュを行う場合、もちろん相互作用はそれほど明確ではありません。