2019-05-21  Java プログラミング

JavaFXでUIスレッドの処理が完了するまで待機する

JavaFX はスレッドセーフではありません。

これは JavaFX に限ったことではなく 世の中に存在するほぼすべての UI ツールキットはシングルスレッドモデルとして設計されており スレッドセーフではありません。AWT Swing Android .NET Framework WindowsForms WPF Linux を含むクロスプラットフォームの GTK いずれもスレッドセーフではないのです。WPF は独立した描画スレッドを持っていますが 開発者が触れるのは UI スレッドのみであり他のツールキットと同様の UI スレッド制御が必要であることからシングルスレッド UI として分類しました

シングルスレッドモデルの UI ツールキットでは UI を操作できるスレッドが 1 つに制限されています。このスレッドの呼び方は様々で JavaFX では FX アプリケーションスレッド AWT Swing では イベントディスパッチスレッド EDT Android WindowsForms GTK では メインスレッド と呼ばれています。これらの UI 操作可能なスレッドを総称して UI スレッド と呼ぶこともあります。

UI にアクセスできるスレッドは 1 つに制限されているため UI 操作を UI スレッドで実行するための手段が UI ツールキットに用意されているのが一般的です。

Swing

Swing には SwingUtilities.invokeLater メソッドと SwingUtilities.invokeAndWait メソッドがあります。どちらもワーカースレッドから呼び出すメソッドで 引数として渡した Runnable UI スレッドで実行してくれます。

  • invokeLater メソッドの呼び出しは Runnable の完了を待たず すぐに復帰します。
  • invokeAndWait メソッドの呼び出しは Runnable が完了するまでブロックされます。

JavaFX

JavaFX には Platform.runLater メソッドが用意されています。

  • runLater メソッド呼び出しは Runnable の完了を待たず すぐに復帰します。

残念ながら JavaFX には SwingUtilities.invokeAndWait に相当するメソッドが提供されていません。これでは UI 操作が完了するのを待ってワーカースレッドの処理を進めたいときに困ってしまいます。

というわけで SwingUtilities.invokeAndWait JavaFX runAndWait メソッドを作ってみます。難しいことはありません。CountDownLatch を使って 非同期実行される Platform.runLater の待ち合わせをすれば Runnable の完了まで待機する同期実行バージョンになります。

runAndWait
public static void runAndWait(Runnable runnable) throws InterruptedException, InvocationTargetException { if(Platform.isFxApplicationThread()) { throw new Error("Cannot call runAndWait from the FX Application Thread"); } Throwable[] throwable = new Throwable[1]; CountDownLatch latch = new CountDownLatch(1); Platform.runLater(() -> { try { runnable.run(); } catch(Throwable t) { throwable[0] = t; } finally { latch.countDown(); } }); latch.await(); if(throwable[0] != null) { throw new InvocationTargetException(throwable[0]); } }

これだけです。このコードを適当なユーティリティ クラスに貼り付ければ使えます。

この runAndWait メソッドはワーカースレッドから呼び出す必要があります。このメソッドを JavaFX アプリケーションスレッド UI スレッド から呼び出すとエラーがスローされます。

UIスレッドから呼び出した場合はエラーにする
if(Platform.isFxApplicationThread()) { throw new Error("Cannot call runAndWait from the FX Application Thread"); }

エラーを発生させずに Runnable UI スレッドでそのまま実行するように実装することもできます。

UIスレッドから呼び出した場合はそのまま実行する
if(Platform.isFxApplicationThread()) { runnable.run(); return; }

ですが 私はこの実装を選択しませんでした。開発者は runAndWait メソッドの呼び出し元が UI スレッドなのかワーカースレッドなのかを明確に意識すべきだからです。

現在のスレッドが UI スレッドかどうかよく分からないけど runAndWait を呼べば UI スレッドで実行される

このようなルーズな使い方は 思わぬバグに繋がる危険性が高く許容すべきではありません。開発中やデバッグ時に とりあえず動く よりも エラーで停止する ほうが 最終的には品質の高い安全なプログラムになると信じています。

SwingUtilities.invokeAndWait メソッドも イベントディスパッチスレッド EDT から呼び出してしまった場合は "Cannot call invokeAndWait from the event dispatcher thread" というエラーが発生するようになっています。

JavaFXのソースコードを読んでみると…

実は JavaFX には SwingUtilities.invokeAndWait に相当するメソッド runAndWait が非公開 API として実装されていました。

PlatformImpl.java
public static void runAndWait(final Runnable r) { runAndWait(r, false); } private static void runAndWait(final Runnable r, boolean exiting) { if (isFxApplicationThread()) { try { r.run(); } catch (Throwable t) { System.err.println("Exception in runnable"); t.printStackTrace(); } } else { final CountDownLatch doneLatch = new CountDownLatch(1); runLater(() -> { try { r.run(); } finally { doneLatch.countDown(); } }, exiting); if (!exiting && toolkitExit.get()) { throw new IllegalStateException("Toolkit has exited"); } try { doneLatch.await(); } catch (InterruptedException ex) { ex.printStackTrace(); } } }

この PlatformImpl クラスのパッケージは com.sun.javafx.application であり 特別な事情がなければ 開発者がアクセスすべきクラスではありません。

公開 API である javafx.application.Platform クラスのソースコードを読んでみると こちらにも runAndWait の名残りが見つかります。

Platform.java
// NOTE: Add the following if we decide to expose it publicly // public static void runAndWait(Runnable runnable) { // PlatformImpl.runAndWait(runnable); // }

公開することにしたら このコードを追加してください コメントアウトを外してください と書かれています。runAndWait の実装はできているけど 敢えて非公開という判断をしているようですね。

この JavaFX のソースコードに存在する runAndWait メソッドですが このまま公開 API にするには実装が良くないなあ と私は思いました。気になったのは 2 箇所です。

1 つは JavaFX アプリケーションスレッド UI スレッド から呼び出した際に エラーが発生することなく そのまま Runnable を実行してしまう点です。Swing と同様に続行せずエラーを発生させるほうが安全なのではないかと思います。

もう 1 つは 例外発生時にスタックトレースが標準出力に送られるだけで実質的に例外が握り潰されている点です。

runAndWait の独自実装と JavaFX のソースコードに隠された runAndWait 実装の紹介は以上です。

最終更新日 2024-12-13
この記事を共有しませんか?
ブックマーク ポスト