Async in JavaFX 1.2 Part 3

前回の続きで今回は『RunnableFuture の進捗状況をどのように JavaTaskBase 側に渡すのか』を試してみようと思います。

ファイルの入出力やDBの検索、操作等の比較的コストのかかる非同期処理を行う場合、その進捗状況をプログレスバー等に出力したくなります。
ご存知だとは思いますが、そのための 属性 percentDone, progress, maxProgress が Task や JavaTaskBase には 予め用意されています。
それらの値を JavaFX 1.2 で追加された ProgressBar 等に渡してあげれば簡単に実現できるはずなのですが...
一体 それらの属性に どのように Java でコーディングした RunnableFuture の実装クラスの進捗を反映させればよいのでしょうか。

前回の日記を読まれた方は...
コールバック関数を使えば JavaFX側で非同期処理がコーディングできるので簡単に進捗を反映できるのでは?
と思われるかもしれません。

しかし、そう簡単でもないのです。
と言うのも、進捗状況は、EDT (Event Dispatch Thread:JavaFX の処理を行うメインのスレッド) とは別のスレッドで非同期に実行されている処理をもとに算出しなくてはならないからです。
前回の日記を読まれた方はもうお分かりだと思いますが...
EDT 以外のスレッドで 単純に progress 等の属性を変更して ProgressBar の表示を変えてしまったら...
あの デッドロック の悲劇が!!

しかし、今回も対策は簡単で...
要は EDT で progress 等の属性を変更すれば良いだけなのです。
EDT で処理を実行するには FX.deferAction(function():Void) を使います。(hide1080 さんのコメントに感謝!!)

で 実装ですが、progress と maxProgress を変更するロジックは ProgressTask#update(Long, Long) に実装されています。この関数の内部では FX.deferAction(function():Void) を呼び出し、progress と maxProgreess を更新しています。 これにより、別スレッドで実行されている ActionTask の action 関数内で update 関数を呼び出しても EDT で処理されるというわけです。

また、今回 再利用性も考慮して、ActionTask に直接実装を追加するのではなく、新たに ProgressTask を追加してみました。 ProgressTask は Task の Wrapper で task 属性に指定した どんな Task にも進捗状況の更新機能を追加できます。
ただし、あくまでサンプルですので実装は十分ではないです。

ProgressTask.fx ( 今回 新しく追加 )

import javafx.async.Task;
import javafx.util.Math;

public class ProgressTask extends Task {

    public-init var task: Task;

    override var started = bind task.started;
    override var stopped = bind task.stopped;
    override var done    = bind task.done;

    override function start() {
        task.start();
    }

    override function stop() {
        task.stop();
    }

    // EDT で progress と maxProgress の状態を変更する。
    public function update(maxProgress: Long, progress: Long) {
        FX.deferAction(function(): Void {
            this.maxProgress = maxProgress;
            this.progress    = Math.min(progress, maxProgress);
        });
    }
}

ActionTaskImpl.java ( LongLongTaskImpl.java 改め )

import javafx.async.RunnableFuture;

public class ActionTaskImpl implements RunnableFuture {

    private  Runnable runnable;

    public ActionTaskImpl() {}

    public ActionTaskImpl(Runnable runnable) {
        this.runnable = runnable;
    }

    @Override
    public void run() {
        if (runnable != null) {
            runnable.run();
        }
    }
}

ActionTask.fx ( LongLongTask.fx 改め )

import java.lang.Runnable;
import javafx.async.JavaTaskBase;

public class ActionTask extends JavaTaskBase {

    public-init var action: function():Void;

    override function create() {
        new ActionTaskImpl(Runnable {
            override function run() {
                action();
            }
        });
    }
}

Main.fx

import java.lang.Thread;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ProgressBar;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.util.Math;

var task: ProgressTask;

Stage {
  scene: Scene {
    content: VBox {
      translateX: 5, translateY: 5
      spacing: 5
      content: [
        HBox {
          spacing: 5
          content:[
            ProgressBar {
              progress: bind Math.max(0, task.percentDone / 100)
              height: 12
            }
            Text {
              content: bind "{Math.max(0, task.percentDone as Integer)}%";
            }
          ]
        }
        HBox {
          spacing: 5
          content: [
            Button {
              text: "Start"
              disable: bind task.started and not task.stopped and not task.done
              action: function() {
                task = ProgressTask {
                  task: ActionTask {
                    action: function() {
                      for (progress in [1..100]) {
                        Thread.sleep(100);
                        task.update(100, progress);
                      }
                    }
                  }
                }
                task.start();
              }
            }
            Button {
              text: "Cancel"
              action: function() {
                task.stop();
                task.update(100, 0);
              }
            }
          ]
        }
      ]
    }
  }
}