非同期処理でJavaのスレッドを走らせることができますか

目的

JavaFXスクリプトからJavaで実装された処理を非同期処理として実行したい場合があります。 例えば、Twitterのつぶやきを再帰的に読み込み続けるような処理とか、 deliciousからすべてのブックマークを読み込むような処理とかです。 このTipsでは、Javaで実装されたこれらの処理が既にあるものと仮定し、 JavaFXスクリプトから非同期処理として起動する方法(その中で私が知っている方法) を紹介します。

このTipsの例はBookmarkというアプリケーションから適宜必要な部分を抜粋・加工して 貼り付けてあります。ソースの全体はsvnにあります。

ソース:
http://sourceforge.jp/projects/tadotter/svn/view/trunk/Bookmark/

必要なプログラム

非同期処理に必要な3つのプログラムです。 ここではメイン、タスク、ランナーと称して区別します。

メイン

あなたが作ろうとしているJavaFXアプリケーションそのものです。 .fxの拡張子を持ち、JavaFXスクリプトで実装します。 メインはタスクをstart(開始)させます。 キャンセルボタンの用意があるならタスクをstop(停止)させればいいです。 明示的にstopさせる場面は明示的な中断のときだけです。 タスクの処理結果を受け取って表示したり、 タスクが処理に失敗した場合は、エラーメッセージを表示したりするでしょう。

タスク

JavaFXの非同期の機構と、Javaで実装された非同期処理を媒介する立場です。 抽象クラスのjavafx.async.JavaTaskBaseを拡張したクラスを作る必要があります。 .fxの拡張子を持ち、JavaFXスクリプトで実装します。 非同期処理の中心は既に実装済みのJavaプログラムですが、 このタスクはそれ以外の重要な雑務を請け合います。 それは非同期処理するJavaインスタンスの生成、 その前処理、その後処理、終わったかどうか、成功か失敗か、失敗の原因、 処理の進捗率(以降プログレスと呼ぶ)などです。

タスクで一番肝心な役割は、非同期処理したいJavaのインスタンスを JavaFXの非同期の機構に引き渡すことです。 その宣言がcreate関数であり、Javaのインスタンスを戻り値とします。 そのインスタンスはどんなインスタンスでもいいわけではありません。 所定のJavaインターフェースを実装している必要があります。 それが後で述べるランナーです。

create関数はJavaFXの非同期の機構が呼び出すので、 あなたが呼び出してはいけません。

タスクには処理結果をメインに渡す変数や関数の用意がありません。 同様にランナーにも処理結果をタスクに渡す仕組みもありません。 これらは自分で実装してください。 処理の過程で結果を(できた分だけ)受け取るなら、Javaのキュー (java.util.concurrent.BlockingQueueなど)を渡して置くのも良いでしょう。 処理が終わってからまとめて結果を受け取るならもっと単純な オブジェクトの受け渡しだけで良いでしょう。 処理結果もそうですが、非同期処理に対して渡したいオブジェクト (検索条件や最大実行回数など)があれば、 それも自分で実装してください。JavaFXの機構としては用意がありません。

ランナー

既にある非同期の核たるJavaプログラムに、 Javaインターフェースのjavafx.async.RunnableFutureを実装してください。 メソッドはrunメソッドひとつだけです。 もちろん.javaの拡張子を持ち、Java言語で実装します。 既にあるクラスに新たなインターフェースを実装するのは嫌かも知れません。 それなら同インターフェースの実装した小さいクラスを書いて、 runメソッドからその核たるJavaを呼び出してください。 runメソッドを呼び出すのはJavaFXの非同期の機構の役目なので、 あなたが呼び出してはいけません。

メイン・ランナー間の情報の受け渡し

メイン>タスク

メインとタスクでの情報の受け渡しはオブジェクト変数を使います。 メインもタスクもどちらもJavaFXなので問題ないでしょう。

例:タスク

public var username:String

タスク>ランナー

ランナーをインスタンス化したのはタスクです。 ランナーのコンストラクタに引数を追加してもいいですし、 ランナーにセッターを設けてもいいでしょう。 いずれにせよcreate関数の中で完了してください。 他にタイミングがありません。

例:タスク

public override function create(): RunnableFuture {
  runner = new BookmarkRunner(username);
  runner
}

例:ランナー

public BookmarkRunner(String username){
  delicious = new Delicious();
  delicious.setUsername(username);
}

ランナー>Java非同期処理

タスクから渡された情報をrunメソッド内でJava非同期処理に渡してください。 ここはJava to Javaなので特に問題はないでしょう。

Java非同期処理>ランナー

ランナーのrunメソッドは戻り値がvoidなので、Java非同期処理の処理結果は ローカルなメンバに保存しておきましょう。 そして、ゲッターを用意してタスクから取り出せるようにしておきましょう。

例:ランナー

private List<Tag> tags;
@Override
public void run() throws Exception{
  ...
  for(...){
    tags.add(tag);
  }
}

public List<Tag> getTags(){
  return tags;
}

ランナー>タスク

タスクがランナーから処理結果を受け取るというよりも、 メインがそれを欲しがるので、 そのための媒介の関数を用意しておくことになります。 もちろんタスクにどこまでの役割を任せるかにもよりますが。 関数を用意する方が、オブジェクト変数に保存してあげるより簡単です。

例:タスク

public function getTags():List{
  runner.getTags()
}

タスク>メイン

メインはタスクに用意された関数を使って処理結果を取り出せます。 そのタイミングはタスクのonDoneが適当でしょう。

例:メイン

var task:BookmarkTask = BookmarkTask{
  onDone:function() {
    if(not task.failed){
      createNewCategories(task.getTags());
    }
  }
}

例外の捕捉

ランナーのrunメソッドはExceptionを投げることができるので、存分に投げてください。 タスクはそれをfailed(失敗)としてキャッチし、 また投げられた例外もcauseOfFailure(失敗の原因)として捉える仕組みになっています。 しかしExcepitonを投げてしまっては、事件の詳細がさっぱりわからないので もっと具体的な例外を投げる必要があるでしょう。 例外が上がらなければタスクはfailedの裏としてsucceeded(成功)とみなします。 メインはタスクのオブジェクトリテラルのonDoneで、 failedならエラーメッセージを表示するような処理を書くことができます。

これは余談ですが、ランナーが正常に処理しても、その処理結果から Nodeを作る段階でメインがOutOfMemoryErrorになる可能性はあります。 それはこのTipsの範疇ではありませんので省きますがご注意ください。 最大実行回数を設けて事前に予防する手なり、 やるだけやらせて諦めてエラー表示する手なりがあるでしょう。

例:ランナー

@Override
public void run() throws Exception{
  //実験用
  String username = delicious.getUsername();
  if(username.equals("_filenotfound")){
      throw new FailedGetPageException(null,FailedGetPageException.Type.FileNotFound);
  }else if(username.equalsIgnoreCase("_connect")){
      throw new FailedGetPageException(null,FailedGetPageException.Type.Connect);
  }else if(username.equalsIgnoreCase("_io")){
      throw new FailedGetPageException(null,FailedGetPageException.Type.IO);
  }else if(username.equalsIgnoreCase("_parse")){
      throw new FailedGetPageException(null,FailedGetPageException.Type.Parse);
  }
}

例:タスク

override var causeOfFailure on replace{
  if(causeOfFailure != null and causeOfFailure instanceof FailedGetPageException){
    var e:FailedGetPageException = causeOfFailure as FailedGetPageException;
    if(e.getType() == FailedGetPageException.Type.Connect or 
      e.getType() == FailedGetPageException.Type.IO){
      errorMessage = "Network Error!";
    }else if(e.getType() == FailedGetPageException.Type.FileNotFound){
      errorMessage = "Page Not Found!";
    }else if(e.getType() == FailedGetPageException.Type.Parse){
      errorMessage = "HTML Parse Error!";
    }
  }else{
    errorMessage = "Sorry! unexpected error has occurred."
  }
}

例:メイン

var task:BookmarkTask = BookmarkTask{
  onDone:function() {
    if(task.failed){
      println(task.errorMessage);
    }
  }
}

処理の中断

メインからタスクに対してstop(停止)の指示があったとき、 ランナーのスレッドは割り込まれた状態に追い込まれます。 それはユーザからの明示的な処理中断の意思に基づく状態なので、 ランナーはそれに気づくべきです。処理のポイントポイントで 今、自分(のスレッド)は割り込まれていないかチェックして そのやり掛けの処理を迅速かつ安全に終了すべきです。 終了はreturnすればいいのです。 タスクにはcauseOfFailure(失敗の原因)にjava.lang.InterruptedException がキャッチされます。 causeOfFailureをon-replaceでウォッチすれば中断を受け付けたことがわかります。

例:ランナー

@Override
public void run() throws Exception{
  Thread thread = Thread.currentThread();
  for(...){
    ...
    if(thread.isInterrupted())return;
  }
}

例:タスク

public-read var errorMessage:String;
override var causeOfFailure on replace{
  if(causeOfFailure != null and causeOfFailure instanceof InterruptedException){
    errorMessage =  "canceled loading";
  }
}

例:メイン

var task:BookmarkTask = BookmarkTask{
  onDone:function() {
    if(task.failed){
      println(task.errorMessage);
    }
  }
}

Button {
  text:"Cancel"
  action:function(){
    task.stop()
  }
}

プログレス

タスクのprogress(プログレス)の仕組みは割り算以外何もしてくれませんが利用すると良いでしょう。 分母たるmaxProgress(全体の仕事の量)と、 分子たるprogress(現在終わっている仕事の総量)を随時設定すれば、 percentDone(終わった仕事の率をパーセント表現)が更新されるようになっています。 メインはpercentDoneを参照することで 現在何パーセント進捗しているのかを把握することができます。

ランナー>タスク

ランナーからタスクにmaxProgressやprogressを伝えるにはどうすれば良いでしょうか。 タスクがランナーをbindする手を最初に思い付くかも知れませんが、これは効きません。 私はJavaのオブザーバモデルを使って解決しました。

監視者はタスクです。被監視者はランナーです。次のようなシナリオになります。

  1. タスクは監視者なので、java.util.Observerインターフェースを実装する。
  2. ランナーは被監視者なので、java.util.Observableを拡張する。
  3. タスクは、ランナーに監視者たる自身を渡す。
  4. ランナーは、渡されたタスクを自分の監視者として登録する。
  5. ランナーは、やるべき仕事の総量をタスクに通知する。
  6. タスクは、通知を受けてmaxProgressを更新する。自動的にpercentDoneが更新される。
  7. ランナーは、完了した仕事の量をタスクに通知する。
  8. タスクは、通知を受けてprogressを更新する。自動的にpercentDoneが更新される。

例:ランナー (extends Observable implements RunnableFuture)

//コンストラクタ
public BookmarkRunner(String username, Observer observer){
  delicious = new Delicious();
  delicious.setUsername(username);
  monitor = new Monitor(); //通知のための自作のPojo
  addObserver(observer);
}

@Override
public void run() throws Exception{
  List<Tag> tagList = delicious.getTags();
  monitor.setTotalWork(tagList.size());
  setChanged();
  notifyObservers();

  int worked = 0;
  for(...){
    ...
    monitor.worked(++worked);
    setChanged();
    notifyObservers(monitor);
  }
}

例:タスク (extends JavaTaskBase, Observer)

override function update(observable:Observable, obj:Object):Void{
  if(obj != null and obj instanceof Monitor){
    def monitor = obj as Monitor;
    maxProgress = monitor.getTotalWork();
    progress = monitor.getWorked();
  }
}

タスク>メイン

メインはタスクのpercentDoneを参照すればいいのです。 percentDoneをプログレスバーの値にbindさせて右へ右へと伸ばしたいでしょう。 しかし、ビジュアルを変化させる場合には単純にバインドしてはいけません。 デッドロックが発生してアプリケーションがフリーズする可能性があります。 これはビジュアル要素に非同期に更新される要素をbindすると、 非同期側のタイミングでメインのビジュアルが動かされる(しかも何度も何度も) ことになり、メイン側が再レイアウトに振り回されるうちにデッドロックしてしまうのです。 非同期側をトリガーにしてレイアウトが変わるようなビジュアルの変化をさせてはいけません。

解決策としては、メインにタイムラインを用意して、 タスクのpercentDoneを1秒おきに参照して プログレスバーの値を更新します。 このタイムラインをタスクのonStartでスタートさせます。 これでビジュアルの再レイアウトはメイン(のスレッド)の配下になり、 デッドロックを防ぐことができます。

例:プログレス用カスタムノード

public function start(task:Task):Void{
  message = "";
  progress = 0;
  visible = true;
  
  def progressWatcher:PauseTransition = PauseTransition{
    duration:1s repeatCount:Timeline.INDEFINITE
    action:function(){
      if(task.done){
        progressWatcher.stop();
      }else{
        progress = Math.max(0, task.percentDone / 100);
      }
    }//action
  }//PauseTransition
  progressWatcher.play();
}//function

例:メイン

var task:BookmarkTask = BookmarkTask{
  onStart:function() {
    popupProgress.start(task);
  }
}

タスク実行中におけるボタンのロック

非同期が走っている間、押されては困るボタンがあるでしょう。 タスクに用意されているステータスをbindして不活性にできます。 プログレスの節では、非同期をビジュアルにbindするなと書きましたが、 これは(今のところ)大丈夫(のよう)です。 ボタンの活性ではなくてラベルを変化させたい場合は、 念のためにLayoutInfoを使ってボタンのサイズを固定すると良い かも知れません。

例:タスク実行中は不活性になるボタン

Button{
  ...
  disable:bind (task == null) or (task.started and not task.done);
}

例:タスク実行中はラベルが変化するボタン

Button{
  text:bind if(task == null or task.done)"Reload" else "Cancel"
  layoutInfo:LayoutInfo{width:60}
}


カウンター

Home