フォーク/ジョイン

Java SE 7リリースに新しく追加されたフォーク/ジョイン・フレームワークは、ExecutorServiceインタフェースの実装です。このインタフェースを使用すると、複数のプロセッサを活用できます。 このフレームワークは、再帰的に小さな単位に分割できる作業向けに設計されています。 利用可能なすべての処理能力を使用して、アプリケーションで最高の速度を達成することを目標としています。

他のExecutorServiceと同様に、フォーク/ジョイン・フレームワークではスレッド・プール内のワーカー・スレッドにタスクを分散します。 フォーク/ジョイン・フレームワークは、ワークスティーリング・アルゴリズムを使用する点で特徴的です。 仕事がなくなったワーカー・スレッドは、ビジー状態の他のスレッドからタスクをスティールする(盗む)ことができます。

フォーク/ジョイン・フレームワークの中心となるのは、AbstractExecutorServiceの拡張クラスである、ForkJoinPoolクラスです。 ForkJoinPoolは、中核的なワークスティーリング・アルゴリズムを実装しており、ForkJoinTaskを実行できます。

基本的な使用法

フォーク/ジョイン・フレームワークの使用法はシンプルです。 最初のステップとして、作業の分割を実行するコードを記述します。 コードは次のようになります。

if (my portion of the work is small enough)
  do the work directly
else
  split my work into two pieces
  invoke the two pieces and wait for the results

このコードをForkJoinTaskのサブクラスに含めます。このクラスは通常、より特化したRecursiveTask型(結果を返すことが可能)またはRecursiveAction型のクラスとなります。

ForkJoinTask型のクラスの記述が完了したら、実行すべき作業全体を表すこのオブジェクトを作成し、それをForkJoinPoolインスタンスのinvoke()メソッドに渡します。

ぼかしを使用して明確化

フォーク/ジョイン・フレームワークの動作を理解するため、シンプルな例を考えてみましょう。 画像にシンプルなぼかしを実行することにします。 変換元(source)画像は、整数の配列によって表されます。各整数には、1ピクセルのカラー値が含まれます。 ぼかしを入れた変換先(destination)画像も、変換前と同じサイズの整数の配列によって表されます。

ぼかしは、変換元の配列全体に対して、一度に1ピクセル分の処理を行うことで実行します。 各ピクセルは、その周囲のピクセルによって平均化され(赤、緑、青の各コンポーネントが平均化されます)、結果が変換先の配列に格納されます。 この場合の実装方法として、次のようなものが考えられます。

public class ForkBlur extends RecursiveAction {
  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;
  }

  protected void computeDirectly() {
    int sidePixels = (mBlurWidth - 1) / 2;
    for (int index = mStart; index < mStart + mLength; index++) {
      // Calculate average.
      float rt = 0, gt = 0, bt = 0;
      for (int mi = -sidePixels; mi <= sidePixels; mi++) {
        int mindex = Math.min(Math.max(mi + index, 0), mSource.length - 1);
        int pixel = mSource[mindex];
        rt += (float)((pixel & 0x00ff0000) >> 16) / mBlurWidth;
        gt += (float)((pixel & 0x0000ff00) >>  8) / mBlurWidth;
        bt += (float)((pixel & 0x000000ff) >>  0) / mBlurWidth;
      }
      
      // Re-assemble destination pixel.
      int dpixel = (0xff000000     ) |
                   (((int)rt) << 16) |
                   (((int)gt) <<  8) |
                   (((int)bt) <<  0);
      mDestination[index] = dpixel;
    }
  }
  
  .
  .
  .

次に、compute()抽象化メソッドを実装します。このメソッドは、直接ぼかしを実行するか、またはその処理を2つの小さなタスクに分割します。 配列の長さのシンプルなしきい値を使用して、ぼかしを実行するか、分割するかを判断します。

  protected static int sThreshold = 100000;

  protected void compute() {
    if (mLength < sThreshold) {
      computeDirectly();
      return;
    }
    
    int split = mLength / 2;
    
    invokeAll(new ForkBlur(mSource, mStart,         split,           mDestination),
              new ForkBlur(mSource, mStart + split, mLength - split, mDestination));
  }

これまでのメソッドがRecursiveActionクラスのサブクラス内にある場合は、それらをForkJoinPoolで実行するよう設定するのは簡単です。

実行すべき作業全体を表すタスクを作成します。

// source image pixels are in src
// destination image pixels are in dst
ForkBlur fb = new ForkBlur(src, 0, src.length, dst);

タスクを実行するForkJoinPoolを作成します。

ForkJoinPool pool = new ForkJoinPool();

タスクを実行します。

pool.invoke(fb);

ソース・コードの完全版については、ForkBlurクラスを参照してください。この完全版には、変換元の画像と変換先の画像をウィンドウで表示するその他のコードも含まれます。

▲ ページTOPに戻る