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);
              }
            }
          ]
        }
      ]
    }
  }
}

Async in JavaFX 1.2 Part 2

前回の日記JavaFX 1.2 になって一新された非同期 API を試してみましたが....
いくつか気になった点があったので更に試してみました。

気になった点とは...

  • 非同期処理のロジックを JavaFX側 でコーディングできないか
  • RunnableFuture の進捗状況をどのように JavaTaskBase 側に渡すのか ( 前回のコメント 参照 )

です。

では、まずは 『非同期処理のロジックを JavaFX でコーディングできないか』 から...
非同期処理のロジックは JavaFX ではなく、Javaで実装しなくてはならないようですが、JavaFX を使っているのに、非同期処理の度に Java のコーディングをするのも ちょっとイマイチな感じ...

と言うことで...
前回のサンプルを コールバック関数 action を使ってちょっと改善してみました。
以下のようにすれば、JavaFX で実装した action 関数を Java に渡して非同期で実行できるようになるのです。
関数を変数と同様に扱えるという JavaFX の特徴をちょっと使ってみました。
とっても簡単なコードですが かなり便利...

しかし こんな簡単なコードの裏にも とてつもない落とし穴が潜んでいるのです。
気をつけないと 思いっきりハマります。私のように...

既にご存知の方もいるかもしれませんが、実は 以下のコード...
LongLongTask の action で Node を変更すると、恐ろしいことに スレッド がデッドロックするのです。
こうなってしまうと、アプリケーションは完全に固まってしまい 強制終了するしかありません。

しかし解決策はとても簡単!!
Node の変更は LongLongTask の onStart, onDone で行おう!!

Main.run(String[]) 及び LongLongTask の action, onStart, onDone は以下のスレッドで実行されています。

[Main.run] Thread[AWT-EventQueue-0,6,main]
[action  ] Thread[task.1,6,task thread pool]
[onStart ] Thread[AWT-EventQueue-0,6,main]
[onDone  ] Thread[AWT-EventQueue-0,6,main]

見て解かるように onStart と onDone は Main.run(String[]) と同じ EDT (Event Dispatch Thread) で実行されています。 当たり前のことですが 同じスレッド (つまり シングルスレッド) で処理している限り デッドロックすることはありません。

更に今回試してみて解かったことですが...
JavaFX 1.1 では 非同期処理を行うスレッドはリソースが許す限り無制限?に立ち上がりましたが、JavaFX 1.2 では Task がプールされるようになり、そのプールされた Task を処理するワーカースレッドの数は 最大8個までになったようです。この数を変更できるかどうかは不明です。WEBアプリと違ってクライアントアプリなら8個もあれば十分ということなのでしょうか...

LongLongTaskImpl.java

import javafx.async.RunnableFuture;

public class LongLongTaskImpl implements RunnableFuture {

    private  Runnable runnable;

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

    @Override
    public void run() {
        runnable.run();
    }
}

LongLongTask.fx

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

public class LongLongTask extends JavaTaskBase {

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

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

Main.fx の一部 変更した箇所

Timeline {
    repeatCount: Timeline.INDEFINITE
    autoReverse: true
    keyFrames: [
        KeyFrame {
            time: 0s
            action: function() {
                var task: LongLongTask;
                task = LongLongTask {
                    var _result: java.util.Date;
                    action: function() {
                        try {
                            java.lang.Thread.sleep(10000);
                        } catch (ex: java.lang.InterruptedException) {}
                        _result = new java.util.Date();
                    }
                    onDone: function() : Void {
                        result = _result;
                    }
                };
                task.start();	
            }
        },
        KeyFrame {
            time: 5s
        }
    ]
}.play();

ということで...
ちょっと書くのも疲れてきたので今回はここまで...
次回 『RunnableFuture の進捗状況をどのように JavaTaskBase 側に渡すのか』 へ続く...