ユーザーインターフェイススレッド
ユーザーインターフェイススレッド (英: user-interface thread) とは、アプリケーションソフトウェアのグラフィカルユーザーインターフェイス (GUI) におけるメインスレッドのことを指す。UIスレッドと表記されることもある。本項では、GUIでのマルチスレッドに関するデザインパターンを記載する。ユーザーインターフェイススレッドは、Javaではイベントディスパッチスレッド、Adobe Flashではprimordial workerと呼ばれる。
歴史的経緯
[編集]GUIアプリケーションにおいて、応答性を維持するためには、基本的にフレームの描画およびユーザー応答(さまざまなユーザー入力に対するアクション)はできるかぎり高いフレームレートで処理しないといけない[注釈 1]。例えば60fps (frames per second) の場合、1フレームの処理を1/60秒=約16ミリ秒以内に完了する必要がある。しかし、たとえどれほどプロセッサの性能が向上したとしても、I/O処理や画像・動画のデコード、通信接続の確立など、完了までに長時間かかってしまう処理は必ず存在する。そういったものを含めて、すべての処理を1つのスレッドだけで実行すると、長時間かかる処理を実行している間はフレーム描画やユーザー応答の処理ができないため、UIの反応がなくなってしまう(ハングアップ、フリーズ)[1][2]。長時間かかる処理の途中に、フレーム描画やユーザー応答に関わるメッセージ処理をときどき挟みながら、長時間かかる処理を少しずつ進める、という方法もあるが[3][4][5]、スマートでないうえに適用限界がある。応答性の低下を防ぐためにマルチスレッド化が必要だが、それをどのようにしてソフトウェアの構造設計に持ち込むか、ということに関して、歴史的には1980年代から色々と議論があった。
例えば、JavaのAWTでは、1996年の最初の時点では、単純にスレッド間でデータ共有型のマルチスレッドになっていた。しかし、データ共有するには、ロックをかけないといけないが、親コンポーネントから子コンポーネントを呼んだり、コールバックで子から親を呼んだり、アプリケーションからGUIライブラリを呼んだり、GUIライブラリからアプリケーションをコールバックしたりと、双方向に呼び出すことが多く、異なるスレッド間で双方向に呼び合うときは、ロックの順番に注意を払う必要がある。これはソフトウェアが非常に複雑になる原因となってしまう。また、ロック順序のミスが引き起こすデッドロックは常にではなくたまに発生したりすることの多いバグ(時間的確率要因が関与する偶発性のあるバグ)であり、バグ取りが大変になるという問題があった[6]。
そこで、1997年のJavaのSwingからは、UIの操作は全てメインのUIスレッドであるイベントディスパッチスレッドから操作しなくてはならない、というルールを設けた。そして、2006年の Java 6 から、UIスレッドで重い処理をすることを避けるために、ワーカーデザインパターン(後述)を採用した javax.swing.SwingWorker
を搭載した。
現在[いつ?]では、多くのUIライブラリが、UIスレッドに操作を限定することと、ワーカーデザインパターンの組み合わせを採用している。
GUIの描画に関しては、UIスレッドが実行するケースもあれば、UIスレッドとは別の専用レンダリングスレッドが実行するケースもある[7][8]。
アプリケーションのメインスレッドは基本的にUIスレッドと同一・等価だが、特殊なケースとして、メインスレッドとUIスレッドが異なるケースもある[9][10]。
UIスレッドへの委譲
[編集]UIスレッドではないサブスレッドでの処理の進捗や完了を画面表示する場合などは、処理の途中でユーザーインターフェイス要素の操作が必要となる。そういった場面では、サブスレッドからUIスレッドに操作を依頼(委譲)することが、まず必要である。
Java
[編集]Javaの場合は、以下の方法で、UIスレッドに委譲できる。J2SE 1.3以降ではどちらもEventQueue
のメソッドが呼び出される実装となっている。
- 同期処理 -
SwingUtilities.invokeAndWait(Runnable)
,EventQueue.invokeAndWait(Runnable)
- 非同期処理 -
SwingUtilities.invokeLater(Runnable)
,EventQueue.invokeLater(Runnable)
同期処理は、処理が完了するまで待つ。
委譲される処理は、java.lang.Runnable
インターフェイスのrun()
メソッドの実装として記述する。
他のライブラリ
[編集]- .NET Framework/.NET Core
- Windows Forms:
System.Windows.Forms.Control.Invoke()
,System.Windows.Forms.Control.BeginInvoke()
- WPF:
System.Windows.Threading.Dispatcher.Invoke()
,System.Windows.Threading.Dispatcher.BeginInvoke()
- WPF 4.5:
System.Windows.Threading.Dispatcher.InvokeAsync()
- WPF 4.5:
- Windows Forms:
- Windowsランタイム/Windows UI Library:
Windows.UI.Core.CoreDispatcher.RunAsync()
[注釈 2] - Android:
Activity.runOnUiThread(Runnable)
,View.post(Runnable)
,View.postDelayed(Runnable, long)
[注釈 3]
.NETでは処理の委譲にデリゲートが使用されることが多い。
ワーカーデザインパターン
[編集]Java
[編集]javax.swing.SwingWorker
を継承して、実装できる。A→B→Cと処理が進むとき、Bが重い処理とすると、Bを SwingWorker.doInBackground() で、Cを SwingWorker.done() で処理する。
実装例:
SwingWorker<Document, Void> worker = new SwingWorker<Document, Void>() {
@Override
public Document doInBackground() throws IOException {
return loadXML(); // 重い処理
}
@Override
public void done() {
try {
Document doc = get();
display(doc);
} catch (Exception ex) {
ex.printStackTrace();
}
}
};
worker.execute();
doInBackgound() の戻り値を done() の SwingWorker.get() で取り出せる。done() はUIスレッドで動作するが、doInBackgound() はUIスレッドとは別のスレッドで動作する。上記例では、done() の中で読み込んだ XML の表示を行う。
Groovy
[編集]上記の例を Groovy で書く場合は、groovy.swing.SwingBuilder
に doLater(), doOutside(), edt() があり、下記のようによりシンプルに書ける。
doOutside {
def doc = loadXML() // 重い処理
edt { display(doc) }
}
他のライブラリ
[編集]System.ComponentModel.BackgroundWorker
- .NET Framework- タスク並列ライブラリ (TPL: Task Parallel Library) - .NET Framework
- 並列パターンライブラリ (PPL: Parallel Patterns Library) - Visual C++/Windowsランタイム
flash.system.Worker
- Adobe Flashandroid.os.AsyncTask
- Android
同一インスタンスのワーカーを2回以上実行したときに、それは、逐次処理するべきなのか、並列処理するべきなのかという議論があり、UIライブラリによって様々であるが、Androidにおいては、2009年9月のAndroid 1.6までは逐次処理、2009年11月のAndroid 2.0からは並列処理、2011年2月のAndroid 3.0からはデフォルトは逐次処理と、色々と変遷した[12]。並列処理にすると使うときにバグを生みやすくなると言うのが、逐次処理に戻した理由である。なお、ワーカー自体が逐次処理であっても、複数のワーカーインスタンスを作り、別々に実行すれば並列に処理できる。
Android 11以降はandroid.os.AsyncTask
は非推奨 (deprecated) となり、java.util.concurrent
またはKotlinコンカレンシーユーティリティの使用が推奨されている。
.NET言語では、TPLとコルーチンをバックエンドに利用した非同期処理の糖衣構文async/awaitも用意されている。async/awaitを使うことで、重い処理(I/Oのような完了時間が予測できない処理)はいったんサブスレッドに委譲しておき、その処理の完了を受けて後続処理を再開するようなコードを簡潔に記述できる。
タイマー
[編集]UIスレッドで呼び出されるタイマーも多くのライブラリで備わっている。
javax.swing.Timer
- JavaSystem.Windows.Forms.Timer
- .NET Framework/Windows FormsSystem.Windows.Threading.DispatcherTimer
- .NET Framework/WPFWindows.UI.Xaml.DispatcherTimer
- Windowsランタイムflash.utils.Timer
- Adobe Flash
脚注
[編集]注釈
[編集]出典
[編集]- ^ Preventing Hangs in Windows Applications - Win32 apps | Microsoft Docs
- ^ アプリの応答性を維持する | Android デベロッパー | Android Developers
- ^ Idle Loop Processing | Microsoft Docs
- ^ DoEvents function (Visual Basic for Applications) | Microsoft Docs
- ^ Application.DoEvents Method (System.Windows.Forms) | Microsoft Docs
- ^ Multithreaded toolkits: A failed dream?
- ^ スレッド モデル - WPF .NET Framework | Microsoft Docs
- ^ UI スレッドの応答性の確保 - UWP applications | Microsoft Docs
- ^ プロセスとスレッドの概要 | Android デベロッパー | Android Developers
- ^ アノテーションによるコード検査の改善 | Android デベロッパー | Android Developers
- ^ Fixing CoreDispatcher.Invoke – How to Invoke Method in UI Thread in Windows 8 Release Preview - Mikael Koskinen
- ^ AsyncTask | Android Developers