UI-作業スレッド分割
<< 「マルチスレッド開発の傾向と対策」に戻る
概要
これは、UIを処理するスレッドと、実作業を行うスレッドとを分けて、それぞれを同時並行的に行うようにする分割方法である。
この手のスレッド分割が行われるのは、時間を要する処理を行っている間にUIの更新が滞り、ユーザに不快な思いをさせないようにするためである。
例えば、下記のような画面について考えてみる。

|

ここでは仮にUI-作業スレッド分割と命名してみたけど、この考え方は必ずしもUIスレッドと作業スレッドとの分業に限られるものではない。
要は、アプリケーション全体を実行する、寿命の長いメインスレッドが、寿命の短い作業用のスレッドを生成するという考え方なのだ。
ただそれが、たまたまUIの画面更新の問題と絡まって、このパターンで使用されることが多いというだけけなのだ。
|
この不自然なウインドウは、PCを使っている限り誰しもが一度は目にしたことがあるはずである。
こういう事態を防ぐための一手法として、UIの処理と実作業とをそれぞれ異なるスレッドで処理させるという方式がとられる事がある。
理論
ウインドウを表示する、イベントドリブン方式のアプリケーションにおいては、スレッドはその処理時間のほとんど全てをメッセージループの中で費やす。
そして、時々ユーザからの操作があったとき等に、あらかじめ決められたイベントハンドラを実行し、またすぐにメッセージループに戻ってくるという動きを繰り返す。

上記の絵は、メッセージループで、メッセージの取り出しとディスパッチ(対応するイベントハンドラを呼び出す処理のこと)が延々と繰り返される事と、呼び出されたイベントハンドラが実行され、すぐに処理を終了する様を表している。
ここでもし、「イベントの処理」に時間がかかるとしたらどうだろうか。
イベントドリブン方式では、イベントハンドラの処理は極力短時間で終了させ、すぐにメッセージループに制御を戻す必要がある。だが、処理そのものには長大な時間を要する。そういった場合はどうすればいいのか。
時間のかかる処理を別のスレッドにやらせて、スレッドを生成した親のスレッドには素早くメッセージループに戻ってもらえばいい。

この場合、イベントハンドラで行う「イベントの処理」とは、作業スレッドを生成することだけとなる。そして、作業スレッドを生成したらその結果を見届けることなく、そそくさとメッセージループに制御を返してしまう。
生成された作業スレッドは、鉄砲玉のごとく、誰に遠慮気兼ねすることもなく、負荷の高い処理を片付ければいい。でもって、処理が終了したら、作業スレッドはその寿命を終える。
こうすることで、UIの更新を妨げユーザに多大な心理的ダメージを与えることなく、負荷の高い処理を行うことが可能となる。
実装例
抽象的な理屈ばかりでは判りにくいので、簡単なアプリケーションを例にとって説明する。例えば、HDD中のファイルを検索するようなアプリケーションを作るような場合について考えてみる。
単純に考えると、下記のようなコードになる。(test2.zip 13.4KB)
// HDD内のファイルを再帰的に検索して、
// リストボックスにファイル名を登録する。
void foo( CString dir, CListBox *pListBox )
{
WIN32_FIND_DATA fd;
HANDLE h = FindFirstFile( dir + "*", &fd );
if ( INVALID_HANDLE_VALUE == h ) return ;
do {
if ( fd.dwFileAttributes | FILE_ATTRIBUTE_DIRECTORY ) {
// ディレクトリだった場合
CString ws = fd.cFileName;
if ( ws != ".." && ws != "." ) {
// サブディレクトリを検索する
foo( dir + ws + "\\", pListBox );
}
}
else {
// 見つかったファイル名をリストボックスに追加する
pListBox->AddString( dir + fd.cFileName );
}
} while( FindNextFile( h, &fd ) );
FindClose( h );
}
// 画面上のボタンが押下されると呼び出される
void CTest2Dlg::OnButton1()
{
CListBox *p = (CListBox*)GetDlgItem( IDC_LIST1 );
// 非常に時間がかかる関数fooを直接呼び出している
foo( "c:\\", p );
}
|
込み入っていて判りにくいが、ここで重要なのは下記の2点だ。
- 関数fooの処理には非常に時間がかかる。
- ボタン押下のイベントハンドラ内で、直接関数fooを呼び出してしまっている。
これでは、OnButton1()が呼び出された瞬間に、アプリケーションはフリーズすることになる。
では、どうすればいいのか。
OnButton1()の中でスレッドを新しく生成して、そのスレッドで関数foo()を実行させればいい。でもって、OnButton1()を呼び出したスレッドは直ちに処理を終了して、関数を抜ければいい。
そうすれば、UIの更新を止めることなく関数foo()を実行させることが可能になる。
つまり、下記のようなコードになるということだ。( test3.zip 14.9KB)
// HDD内のファイルを再帰的に検索して、
// リストボックスにファイル名を登録する。
void foo( CString dir, CListBox *pListBox )
{
WIN32_FIND_DATA fd;
…… 略 ……
FindClose( h );
}
#include "Thread.h"
// スレッドを生成するためのクラス
class CFoo : public CThread
{
public:
void run()
{
// 生成された別のスレッドで
// 関数foo()を実行する
foo( "C:\\", pListBox );
};
CListBox *pListBox;
} theFoo;
void CTest3Dlg::OnButton1()
{
CListBox *p = (CListBox*)GetDlgItem( IDC_LIST1 );
// スレッドを生成したら、
// すぐにメッセージループに戻る
theFoo.pListBox = p;
theFoo.start();
}
|
変更されたのは、上記で太字になっている部分だ。一見しておぞましいロジックだが、やりたいこととしては単に関数foo()を別スレッドで実行するようにしているだけだ。
このようにすることで、UIの処理を止めることなくHDD内の検索を行わせることが可能となる。
別の実装方式
ここでおもむろに、新たにスレッドを生成することなく、同様の処理を実現する方法について考えてみる。つまり、UIの更新を滞らせることなく、時間のかかる処理を、シングルスレッドで実現するのだ。
シングルスレッドであることと、UIの更新を適宜い行うことを両立させるには、大きく言って下記の2つの方法がある。
- 定期的にメッセージの取り出しとディスパッチを行うコートを挿入する。
- タイマなどで定期的に呼ばれるイベントハンドラで、ちょっとずつ処理を進めるようにする。
1の方法は、例えば上述のHDD検索の問題であれば、関数foo()内のdoの直後に下記のコードを挿入するようにすることである。
MSG msg;
if ( PeekMessage( &msg, NULL, 0, 0, PM_REMOVE ) ) {
TranslateMessage( &msg );
DispatchMessage( &msg );
}
|
Xt/Motifであれば、下記のような記述になる。
XEvent event;
if ( XtAppPeekEvent( appContext, &event ) ) {
XtDispatchEvent( &event );
}
|
これらの記述が何を意味してどんな動きをするのかについては、ここでは解説はしない。
こんなページを書いておいていうのは何だが、俺の経験則からすると、UI更新の問題だけであれば、マルチスレッドにするよりもは、上記のコードを適宜実行するようにした方が明瞭簡潔で良いようだ。ダサいし泥臭いが、圧倒的に簡単である。
2の方法は、一般に言って非常に困難を極める。上記HDD検索の問題を、2の方法で動作するように書き換えることは不可能ではないが、俺はやりたくはない。
だから、ここではあえて触れないことにしておく。
<< 「マルチスレッド開発の傾向と対策」に戻る
|