JavaFX 1.2 非同期処理の概要(3)ー 非同期処理の進捗を通知する

まったく人気のないJavaFXネタですが、当面は続くと思われます。使ってみると思った以上に面白いです。
今回は非同期処理の概要としては最終回。非同期処理の進捗を表示するような場合にどうするかです。これまでの非同期処理は終わった時に関数が呼び出されるため、長い処理が今どのくらいの進捗なのかが解りませんでした。プログレスバーや進捗率などを表示することはRIAとしては必要不可欠な要素の1つかと思いますので、紹介します。
尚、ネタはこちらのBlogからです。また、今後改良される可能性が非常に高いことはお断りしておきます*1
実行サンプルはこちら

進捗をMyLongTaskに伝える

それではタスクの進捗状況をMyLongTaskに伝え、それを表示しましょう。JavaFX 1.2ではこの仕組みを提供するフレームワークは準備されていますが、未完成な状態のようです。現時点では、開発者が色々と実装しなければなりません。最終的にはSwingWorkerのような統一されたAPIが用意されるとは思います。

MyLongTaskHandler.java

最初に作成するのは、MyLongTaskHandlerは進捗に変化があった時に進捗状態を通知するインターフェイスです。このインターフェイスは新しく作成します。

package jp.deathmarch.javafx.sample.async;

public interface MyLongTaskHandler {
    void onProgress(int value);
}
MyLongTaskResultHolder.java

特に変更はありません。

package jp.deathmarch.javafx.sample.async;

import java.util.concurrent.atomic.AtomicReference;

public class MyLongTaskResultHolder {
    private final AtomicReference<String> result
                      = new AtomicReference<String>();
    public String getResult() {
        return result.get();
    }
    public void setResult(String result) {
        this.result.set(result);
    }
}
MyLongWorker.java

次に非同期に実行される処理を記述したクラスです。しつこいようですが、このクラスのrun()メソッドは非EDTで実行されます。忘れないように書いておくべきでしょう。

package jp.deathmarch.javafx.sample.async;

import com.sun.javafx.functions.Function0;
import javafx.async.RunnableFuture;
import javafx.lang.FX;

// 非EDTで実行されるタスク
// 決してGUIの描画に関わるような処理を記述してはいけない
public class MyLongWorker implements RunnableFuture {

    private final MyLongTaskResultHolder result;
    private final MyLongTaskHandler handler;
    public MyLongWorker(MyLongTaskResultHolder result, MyLongTaskHandler handler) {
        this.result = result;
        this.handler = handler;
    }
    
    // このメソッドは非EDTで実行される
    @Override
    public void run() throws Exception {
        System.out.println("MyLongTask start.");
        for (int i = 0; i < 10; i++) {
            try {
                Thread.sleep(500); // sleep 500ms * 10
                onProgress(i+1);
            } catch (InterruptedException e) { // do nothing
            }
        }
        result.setResult("Complete!");
        System.out.println("MyLongTask end.");
    }

    // 進捗状況を通知する。
    // そのまま呼び出ささずにEDTで処理させる
    private void onProgress(final int value) {
        FX.deferAction(new Function0<Void>() {
            @Override
            public Void invoke() {
                handler.onProgress(value);
                return null;
            }
        });
    }
}

コンストラクタの引数にMyLongTaskHandlerを追加しています。進捗状況の通知はこのインターフェイスを通して行います。また、進捗が解りやすいようにsleepは0.5秒毎に10セットに変更しています。それぞれの処理が終わった時にonProgressを呼び出しているだけです。
onProgressメソッドでは、FXクラスのdeferActionメソッドを使い、MyLongTaskHandlerに進捗を通知しています。この部分は次のようにしてはいけません。

    private void onProgress(int value) {
        handler.onProgress(value);
    }

これがマズイ理由はEDT以外からの処理だからです。onProgressではGUIに関連する処理が行われている筈なので、この処理は必ずEDTで行わなければなりません。
Swingではこのような場合、SwingUtils.involeLaterを使用しましたが、JavaFXの場合は、javafx.lang.FX#deferActionを使用します。細かいことは兎も角、『後でEDTでやってくれ』という意味は同じです。
これで、各進捗でEDTを通して進捗が通知されるようになりました。

MyLongTask.fx

修正点はMyLongTaskHandlerを継承するようになった事と、JavaTaskBaseのインスタンス変数の初期化を行っている事です。
JavaFXではmixinが可能な為、インターフェイスでもextends句に記述します。実装に関してはoverrideを使うので差はありません。progressはJavaTaskBaseに定義されたインスタンス変数ですが、ここにハンドラーを経由して通知された値をセットしています。

package jp.deathmarch.javafx.sample.async;

import javafx.async.JavaTaskBase;
import javafx.async.RunnableFuture;

public class MyLongTask extends JavaTaskBase, MyLongTaskHandler {
    init {
      maxProgress = 10; // 進捗の最大値
      progress = 0;     // 初期の進捗値
    }
    var result: MyLongTaskResultHolder;
    // return RunableFuture implement
    override function create(): RunnableFuture {
        result = new MyLongTaskResultHolder();
        return new MyLongWorker(result, this);
    }
    public function getResult():String {
        return result.getResult();
    }
    override function onProgress(value: Integer): Void {
        progress = value;
    }
}

また、initブロックにてmaxProgressとprogressの値を設定しています。どちらもpublic-read protectedなインスタンス変数です。

Main.fx

ほとんど変わりませんが、進捗を表示するTextが追加されています。こちらもbindを使ってあっさりとした記述ですね。

package jp.deathmarch.javafx.sample.async;

import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.text.Text;

import javafx.scene.text.TextOrigin;

var text:String;
var myLongTask:MyLongTask  = MyLongTask {
  onStart: function() {
      text = "onStart";
  }
  onDone: function() {
      text = "onDone: {myLongTask.getResult()}";
  }
};
myLongTask.start();
Stage {
    title: "Async Sample1"
    scene: Scene {
        width: 100, height: 50
        content: [
            Text {
                textOrigin: TextOrigin.TOP
                content: bind text
            }
            Text {
                y: 20
                textOrigin: TextOrigin.TOP
                content: bind "{myLongTask.percentDone}%"
            }
        ]
    }
}

まとめ

3回に渡ってJavaFXの非同期処理を解説してきましたが、基本的には非同期タスクを作成し、フレームワーク側がスレッドの管理を行うようになっています。JavaTaskBaseの実装は公開されていませんが、恐らくは数スレッドを管理するExecutorServiceがいて、そこにRunnableでラップしてsubmitしているのではと思われます。
現時点では定期的な処理を別スレッドで行うようなフレームワークは用意されていませんが、おそらくJavaFX 2.0の頃には実装されるのではないでしょうか?現時点で作る場合でも、それほど労力は使わずに実装できそうです。
また、現時点ではJavaインターフェイスを使わないと非同期処理ができません。これは次のように記述すれば簡単に(ほとんど)JavaFXの世界のみで実現できるため、近い将来用意されると思います。
ActionRunnableFuture.java

public class ActionRunnableFuture implements RunnableFuture {
    private final Function0<Void> func;
    public ActionRunnableFuture(Function0<Void> func) {
        this.func = func;
    }
    @Override
    public void run() throws Exception {
       this.func.invoke();
    }
}

Main.fx

class FxActionTask extends JavaTaskBase {
    public-init var action: function(): Void;
    override function create(): RunnableFuture {
        return new ActionRunnableFuture(action);
    }
}
FxActionTask {
    action: function() {
        java.lang.Thread.sleep(5000);
    }
    onDone: function() {
        println("END");
    }
}.start();

ポイントはFunction0クラスがFX世界の関数をJava世界で扱うためのクラスという点でFX世界のFxActionTaskクラスのaction関数をそのままコンストラクタの引数に渡しているところです。
こうすることで、FxActionTaskを利用する側としては、何を非同期で行うかをactionに定義し、終わったらどうするかをonDoneに定義するだけで済むわけです。

*1:とはいえ仕組みを知るのは重要