ローカルストレージを非同期で

JavaFX 1.2 で変更された javafx.async の非同期処理 API の使い方と、そこではまりがちな問題の回避策について、id:bluepapa32 さんが解説してくれました。
Async in JavaFX 1.2 - bluepapa32’s Java Blog
Async in JavaFX 1.2 Part 2 - bluepapa32’s Java Blog
Async in JavaFX 1.2 Part 3 - bluepapa32’s Java Blog


これをちょっと使わせてもらって、ローカルストレージにデータを保存してみます。ProgressTask を少しだけ変更します。


ProgressTask.java

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

def NO_TASK = new ActionTaskImpl();

public class ProgressTask extends Task {

    public-init var task: Task;
    //override public-init protected maxProgress: Long;

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

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

変更箇所は、update() の処理を javafx.lang.FX の deferAction() を使うようにしています。これならば JavaFX のコア API なので、使っても気持ちが悪いことはないと思いますし、僕個人的にはこっちの方が好きかもしれません。
FX.deferAction() は Java の SwingUtilities.invokeLater() の JavaFX 版と考えられると思います。


Main.fx

import java.lang.*;
import java.io.*;

import javafx.async.*;
import javafx.io.*;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.Tile;
import javafx.scene.text.Font;
import javafx.stage.*;


def prefferedSize = 1048576;

var storage = Storage {
    source: "fugedata.txt"
}
var resource = storage.resource;
resource.maxLength = prefferedSize;
FX.addShutdownAction(function (): Void {
    storage.clear();
});

Stage {
    title : "大きなデータを書き込むテスト"
    scene: Scene {
        content: [
            Tile {
                var button: Button;
                var task: ProgressTask;
                vertical: true
                vgap: 10
                columns: 1
                rows: 2
                content: [
                    button = Button {
                        text: "書き込む"
                        font: Font{}
                        action: function() {
                            button.disable = true;
                            task = ProgressTask {
                                task: ActionTask {
                                    action: function() {
                                        var os = new BufferedOutputStream(resource.openOutputStream(false));
                                        var i = 0;
                                        while (true) {
                                            os.write(97); //write 'a'
                                            i++;
                                            if ((i mod 1024) == 0) {
                                                task.update(resource.maxLength, i);
                                                Thread.sleep(10);
                                            }
                                            if (resource.maxLength == i) {
                                                task.update(resource.maxLength, resource.maxLength);
                                                break;
                                            }
                                        }
                                        os.close();
                                    }
                                    onDone: function() {
                                        Alert.inform("ローカルストレージの非同期処理", "保存が完了しました。");
                                    }
                                }
                            }
                            task.start();
                        }
                    }
                    ProgressBar {
                        progress: bind task.percentDone / 100;
                    }
                ]
            }
        ]
    }
}

これでやっていることは、ActionTask action() の中で javafx.io API を使って、ローカルストレージに 1M バイト分のデータ(単純に文字 'a' だけ)を書き込んでいって、1024k バイト毎に ProgressTask update() を呼び出して、ProgressBar を更新するようにしています。Thread.sleep(10) は、単に ProgressBar の状態が更新されるのを見やすくするために入れているだけなので、実際に使う場合は、不要かもしれません。
これでデッドロックすることなく、進捗状況を把握することができるようになります。



※ ローカルコンピュータ上に 1M (1048576) バイトのデータを書き込みます。ウィンドウ右上の×ボタン押下して終了させるとデータを削除するようにしています。


追記:
因みに ActionTask の action() と ProgressTask の update() はメソッド名がそれぞれの処理をよく表していて良いのですが、同じ名前にして、デコレータ(Decorator パターン)みたいな形にするというのはどうなんでしょうね。更に別機能を持つ Task もラップして同一視して扱うというのも良いかも知れないなんて思ってみたり。今現在そんな Task は思い当たりませんが...