ユーザー・インターフェイス・スレッドはユーザーの要求に反応する必要があり、長い計算の実行中でも反応しなければならない。
グラフィカル・ユーザー・インターフェイスには、多くの場合、ユーザーの操作を処理する専用のスレッド (GUI スレッド) があります。 アプリケーションが長い計算を実行している間であっても、スレッドはユーザーの要求に反応する必要があります。 例えば、ユーザーが実行中の計算を中断するために「キャンセル」ボタンを押すことがあります。 GUI スレッドがこの計算に関係している場合、ユーザーの要求に即座に反応することができません。
GUI スレッドはイベントループをサービスします。
GUI スレッドはほかのスレッドに作業を渡し、その作業の完了を待機しないようにする必要があります。
GUI スレッドはイベントループに反応しなければならず、渡された作業を行うことに専念してはなりません。
GUI スレッドは、task::enqueue メソッドを使用してタスクに作業を渡します。 作業が完了すると、タスクは作業が完了したことを知らせるイベントを GUI スレッドにポストします。 enqueue メソッドのセマンティクスにより、タスクは呼び出しスレッドとは異なるワーカースレッドで実行されます。 このメソッドは、インテル® スレッディング・ビルディング・ブロック (インテル® TBB) 3.0 で追加されました。
次の図は、通信パスを図で示したものです。黒の項目は GUI スレッドで実行され、青の項目は別のスレッドで実行されます。
このサンプルは Microsoft* Windows* オペレーティング・システム向けですが、同様の原理はイベントループを使用する任意の GUI に適用できます。 イベントごとに、GUI スレッドはユーザー定義関数 WndProc を呼び出してイベントを処理します。 重要な部分は太字で表記しています。
// 作業完了時にキューに入れられたタスクからポストされるイベント
const UINT WM_POP_FOO = WM_USER+0;
// キューに入れられたタスクから GUI スレッドに結果を送信するためのキュー
tbb::concurrent_queue<Foo>ResultQueue;
// GUI スレッドの最新の計算結果のプライベート・コピー
Foo CurrentResult;
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) {
switch(msg) {
case WM_COMMAND:
switch (LOWORD(wParam)) {
case IDM_LONGRUNNINGWORK:
// ユーザーが要求した長い計算を別のスレッドに依頼
LaunchLongRunningWork(hWnd);
break;
case IDM_EXIT:
DestroyWindow(hWnd);
break;
default:
return DefWindowProc(hWnd, msg, wParam, lParam);
}
break;
case WM_POP_FOO:
// ResultQueue に処理すべき別の結果がある
ResultQueue.try_pop(CurrentResult);
// 最新の結果でウィンドウを更新
RedrawWindow( hWnd, NULL, NULL, RDW_ERASE|RDW_INVALIDATE );
break;
case WM_PAINT:
CurrentResult を使用してウィンドウを再描画
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc( hWnd, msg, wParam, lParam );
}
return 0;
}
GUI スレッドは長い計算を次のように処理します。
GUI スレッドは LongRunningWork を呼び出して作業をワーカースレッドに依頼します。
GUI スレッドはイベントループのサービスを続けます。ウィンドウの再描画が必要な場合、Foo が最後に処理した最新の値である CurrentResult を使用します。
ワーカーが長い計算を終了すると、結果を ResultQueue にプッシュして、GUI スレッドに WM_POP_FOO メッセージを送信します。
GUI スレッドは ResultQueue から CurrentResult に項目をポップして WM_POP_FOO メッセージを処理します。 ResultQueue の各項目に WM_POP_FOO メッセージが 1 つあるため、try_pop は常に成功します。
LaunchLongRunningWork ルーチンはルートタスクを作成し、task::enqeueue メソッドを使用してタスクを起動します。 待機している後続のタスクがないため、このタスクはルートタスクです。
class LongTask: public tbb::task {
HWND hWnd;
tbb::task* execute() {
Do long computation
Foo x = result of long computation
ResultQueue.push( x );
// 結果が利用可能であることを GUI スレッドに通知
PostMessage(hWnd,WM_POP_FOO,0,0);
return NULL;
}
public:
LongTask( HWND hWnd_ ) : hWnd(hWnd_) {}
};
void LaunchLongRunningWork( HWND hWnd ) {
LongTask* t = new( tbb::task::allocate_root() ) LongTask(hWnd);
tbb::task::enqueue(*t);
}
task::spawn メソッドではなく task::enqueue メソッドを使用する必要があります。 その理由は、enqueue メソッドは、タスクを明示的に待っているスレッドがない場合でも、リソース的に問題がなければタスクを実行しますが、 spawn メソッドは、明示的に待機するスレッドが無ければタスクの実行を延期するためです。
サンプルでは、concurrent_queue を使用してワーカースレッドから GUI スレッドに結果を返しています。 サンプルでは最新の結果のみ重要ですが、別の方法として mutex で保護されている共有変数を使用する方法もあります。 ただし、この方法では GUI スレッドが mutex をロックしている間はワーカーがブロックされます。逆も同様です。 このため、concurrent_queue を使用するほうが、単純かつ優れたソリューションです。
2 つの長い計算を実行したときに、最初の計算が 2 つめの計算の後に完了する可能性があります。 最後に要求した計算の結果を表示することが重要であれば、要求にシリアル番号を割り当て、計算と関連付けます。 GUI スレッドは ResultQueue をポップして一時変数に入れ、シリアル番号を確認して、シリアル番号が進む場合のみ CurrentResult を更新します。