GUI スレッド#
問題
ユーザー・インターフェイス・スレッドは、ユーザーの要求に応答し続ける必要があり、長時間の計算で停滞してはなりません。
コンテキスト
グラフィカル・ユーザー・インターフェイスには、ユーザー操作を処理する専用スレッド (「GUI スレッド」) が備わっていることがあります。アプリケーションが長時間の計算を実行している間でも、スレッドはユーザーの要求に応答し続ける必要があります。例えば、ユーザーは長時間実行される計算を停止するため「キャンセル」ボタンを押すことがあります。GUI スレッドが長時間実行される計算に参加すると、ユーザーの要求に応答できなくなります。
強制
GUI スレッドはイベントループを処理します。
GUI スレッドは、ワークが完了するのを待たずに、ワークを他のスレッドにオフロードする必要があります。
GUI スレッドはイベントループに応答する必要があり、オフロードされたワークの実行専用にならないようにする必要があります。
関連性
ノンプリエンプティブな優先度
ローカル・シリアライザー
解決方法
GUI スレッドは、task_arena インスタンスのメソッド task_arena::enqueue を使用してタスクを起動しワークをオフロードします。完了すると、タスクはワークが完了したことを示すイベントを GUI スレッドにポストします。enqueue セマンティクスにより、タスクは最終的に呼び出したスレッドとは異なるワーカースレッドで実行されることになります。
次の図は通信の流れを示しています。黒の項目は GUI スレッドによって実行され、青の項目は別のスレッドによって実行されます。
例
この例は Microsoft Windows* オペレーティング・システム用ですが、イベントループのイディオムを使用するすべての GUI に同様の原則が適用されます。各イベントごとに、GUI スレッドはユーザー定義関数 WndProc を呼び出してイベントを処理します。
// キューに入れられたタスクがワークを完了したときにポストされるイベント
const UINT WM_POP_FOO = WM_USER+0;
// キューに登録されたタスクから GUI スレッドに結果を送信するキュー
oneapi::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:
Repaint the window using CurrentResult
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default: return DefWindowProc( hWnd, msg, wParam, lParam );
}
return 0;
}GUI スレッドは長い計算を次のように処理します。
GUI スレッドは
LongRunningWorkを呼び出し、ワークをワーカースレッドに渡してすぐに戻ります。GUI スレッドはイベントループの処理を続行します。ウィンドウを再描画する必要がある場合、最後に確認された
FooであるCurrentResultの値を使用します。
ワーカーは長い計算を終了すると、結果を ResultQueue にプッシュし、メッセージ WM_POP_FOO を GUI スレッドに送信します。
GUI スレッドは、ResultQueue から CurrentResult に項目をポップすることで、
WM_POP_FOOメッセージを処理します。ResultQueue内の各項目に対してWM_POP_FOOメッセージが 1 つだけ存在するため、try_popは常に成功します。
ルーチン LaunchLongRunningWork は関数タスクを作成し、メソッド task_arena::enqueue を使用して起動します。
class LongTask {
HWND hWnd;
void operator()() {
Do long computation
Foo x = result of long computation
ResultQueue.push( x );
// 結果が利用可能であることを GUI スレッドに通知
PostMessage(hWnd,WM_POP_FOO,0,0);
}
public:
LongTask( HWND hWnd_ ) : hWnd(hWnd_) {}
};
void LaunchLongRunningWork( HWND hWnd ) {
oneapi::tbb::task_arena a;
a.enqueue(LongTask(hWnd));
}ここでは、task_arena::enqueue メソッドを使用することが重要です。明示的に task_arena インスタンスが作成されていても、enqueue メソッドは、リソースが利用可能になった時点でタスク関数が最終的に実行されることを保証します。これは、タスクに対して明示的に待機するスレッドが存在しない場合でも同様です。対照的に、oneapi::tbb::task_group::run は、oneapi::tbb::task_group::wait で明示的に待機するまで、タスク関数の実行を延期する場合があります。
この例では、ワーカーが結果を GUI スレッドに返すのに、concurrent_queue を使用します。この例では最新の結果のみが重要であるため、代わりにミューテックスで保護された共有変数を使用することもできます。ただし、これを行うと、GUI スレッドがミューテックスのロックを保持している間は、ワーカーがブロックされ、その逆も同様になります。concurrent_queue によって、シンプルで堅牢なソリューションが提供されます。
2 つの長い計算が実行中である場合、最初の計算が 2 番目の計算の後に完了する可能性があります。最後に要求された計算の結果を表示が重要である場合は、要求されたシリアル番号を計算に関連付けます。GUI スレッドは、ResultQueue から一時変数に取得し、シリアル番号を確認してシリアル番号が更新される場合にのみ CurrentResult を更新できます。
