チュートリアル: ユーザー インターフェイス スレッドからの処理の除去
このドキュメントでは、Microsoft Foundation Classes (MFC) アプリケーションのユーザー インターフェイス (UI) スレッドによって実行される処理を、同時実行ランタイムを使用してワーカー スレッドに移動する方法について説明します。 また、時間のかかる描画操作のパフォーマンスを向上させる方法についても説明します。
描画のような他の処理の妨げになる操作をワーカー スレッドにオフロードすることで UI スレッドから処理を除去すると、アプリケーションの応答性を向上させることができます。 このチュートリアルでは、マンデルブロ フラクタルを生成する描画ルーチンを使用して、時間のかかるブロック操作を示します。 また、マンデルブロ フラクタルの生成は、各ピクセルの計算が他のすべての計算とは無関係に行われるので、並列化に適した候補でもあります。
必須コンポーネント
このチュートリアルを開始する前に、次のトピックを参照してください。
また、このチュートリアルを始める前に、MFC アプリケーションの開発および GDI+ の基礎について理解しておくことをお勧めします。 MFC の詳細については、「MFC リファレンス」を参照してください。 GDI+ の詳細については、「GDI+」を参照してください。
セクション
このチュートリアルは、次のセクションで構成されています。
MFC アプリケーションの作成
マンデルブロ アプリケーションのシリアル バージョンの実装
ユーザー インターフェイス スレッドからの処理の除去
描画パフォーマンスの向上
取り消し処理のサポートの追加
MFC アプリケーションの作成
ここでは、基本的な MFC アプリケーションを作成する方法について説明します。
Visual C# MFC アプリケーションを作成するには
[ファイル] メニューの [新規作成] をポイントし、[プロジェクト] をクリックします。
[新しいプロジェクト] ダイアログ ボックスで、[インストールされたテンプレート] ペインの [Visual C++] をクリックし、[テンプレート] ペインの [MFC アプリケーション] をクリックします。 プロジェクトの名前 (Mandelbrot など) を入力し、[OK] をクリックして、MFC アプリケーション ウィザードを表示します。
[アプリケーションの種類] ペインで [シングル ドキュメント] をクリックします。 [ドキュメント/ビュー アーキテクチャのサポート] チェック ボックスがオフになっていることを確認します。
[完了] をクリックしてプロジェクトを作成し、MFC アプリケーション ウィザードを閉じます。
アプリケーションをビルドして実行することにより、アプリケーションが正常に作成されたことを確認します。 アプリケーションをビルドするには、[ビルド] メニューの [ソリューションのビルド] をクリックします。 アプリケーションが正常にビルドされたら、[デバッグ] メニューの [デバッグ開始] をクリックして、アプリケーションを実行します。
マンデルブロ アプリケーションのシリアル バージョンの実装
ここでは、マンデルブロ フラクタルを描画する方法について説明します。 このバージョンは、マンデルブロ フラクタルを GDI+ Bitmap オブジェクトに描画した後、そのビットマップの内容をクライアント ウィンドウにコピーします。
マンデルブロ アプリケーションのシリアル バージョンを実装するには
stdafx.h に次の #include ディレクティブを追加します。
#include <memory>
ChildView.h の pragma ディレクティブの後で、BitmapPtr 型を定義します。 BitmapPtr 型は、Bitmap オブジェクトへのポインターを複数のコンポーネントで共有できるようにします。 Bitmap オブジェクトは、どのコンポーネントからも参照されなくなると削除されます。
typedef std::shared_ptr<Gdiplus::Bitmap> BitmapPtr;
ChildView.h で、CChildView クラスの protected セクションに次のコードを追加します。
protected: // Draws the Mandelbrot fractal to the specified Bitmap object. void DrawMandelbrot(BitmapPtr); protected: ULONG_PTR m_gdiplusToken;
ChildView.cpp で、次の行をコメント アウトまたは削除します。
//#ifdef _DEBUG //#define new DEBUG_NEW //#endif
この手順により、デバッグ ビルドでアプリケーションが DEBUG_NEW アロケーターを使用しないようにします。このアロケーターは GDI+ と互換性がありません。
ChildView.cpp で、using ディレクティブを Gdiplus 名前空間に追加します。
using namespace Gdiplus;
GDI+ の初期化とシャットダウンを行うために、次のコードを CChildView クラスのコンストラクターとデストラクターに追加します。
CChildView::CChildView() { // Initialize GDI+. GdiplusStartupInput gdiplusStartupInput; GdiplusStartup(&m_gdiplusToken, &gdiplusStartupInput, NULL); } CChildView::~CChildView() { // Shutdown GDI+. GdiplusShutdown(m_gdiplusToken); }
CChildView::DrawMandelbrot メソッドを実装します。 このメソッドは、マンデルブロ フラクタルを指定した Bitmap オブジェクトに描画します。
// Draws the Mandelbrot fractal to the specified Bitmap object. void CChildView::DrawMandelbrot(BitmapPtr pBitmap) { if (pBitmap == NULL) return; // Get the size of the bitmap. const UINT width = pBitmap->GetWidth(); const UINT height = pBitmap->GetHeight(); // Return if either width or height is zero. if (width == 0 || height == 0) return; // Lock the bitmap into system memory. BitmapData bitmapData; Rect rectBmp(0, 0, width, height); pBitmap->LockBits(&rectBmp, ImageLockModeWrite, PixelFormat32bppRGB, &bitmapData); // Obtain a pointer to the bitmap bits. int* bits = reinterpret_cast<int*>(bitmapData.Scan0); // Real and imaginary bounds of the complex plane. double re_min = -2.1; double re_max = 1.0; double im_min = -1.3; double im_max = 1.3; // Factors for mapping from image coordinates to coordinates on the complex plane. double re_factor = (re_max - re_min) / (width - 1); double im_factor = (im_max - im_min) / (height - 1); // The maximum number of iterations to perform on each point. const UINT max_iterations = 1000; // Compute whether each point lies in the Mandelbrot set. for (UINT row = 0u; row < height; ++row) { // Obtain a pointer to the bitmap bits for the current row. int *destPixel = bits + (row * width); // Convert from image coordinate to coordinate on the complex plane. double y0 = im_max - (row * im_factor); for (UINT col = 0u; col < width; ++col) { // Convert from image coordinate to coordinate on the complex plane. double x0 = re_min + col * re_factor; double x = x0; double y = y0; UINT iter = 0; double x_sq, y_sq; while (iter < max_iterations && ((x_sq = x*x) + (y_sq = y*y) < 4)) { double temp = x_sq - y_sq + x0; y = 2 * x * y + y0; x = temp; ++iter; } // If the point is in the set (or approximately close to it), color // the pixel black. if(iter == max_iterations) { *destPixel = 0; } // Otherwise, select a color that is based on the current iteration. else { BYTE red = static_cast<BYTE>((iter % 64) * 4); *destPixel = red<<16; } // Move to the next point. ++destPixel; } } // Unlock the bitmap from system memory. pBitmap->UnlockBits(&bitmapData); }
CChildView::OnPaint メソッドを実装します。 このメソッドは、CChildView::DrawMandelbrot を呼び出した後、Bitmap オブジェクトの内容をウィンドウにコピーします。
void CChildView::OnPaint() { CPaintDC dc(this); // device context for painting // Get the size of the client area of the window. RECT rc; GetClientRect(&rc); // Create a Bitmap object that has the width and height of // the client area. BitmapPtr pBitmap(new Bitmap(rc.right, rc.bottom)); if (pBitmap != NULL) { // Draw the Mandelbrot fractal to the bitmap. DrawMandelbrot(pBitmap); // Draw the bitmap to the client area. Graphics g(dc); g.DrawImage(pBitmap.get(), 0, 0); } }
アプリケーションをビルドして実行することにより、アプリケーションが正常に更新されたことを確認します。
次の図は、マンデルブロ アプリケーションの結果を示しています。
各ピクセルの計算には負荷がかかるので、すべての計算が終了するまで UI スレッドは新しいメッセージを処理できません。 これにより、アプリケーションの応答性が低下します。 ただし、UI スレッドから処理を除去することでこの問題を軽減できます。
[ページのトップへ]
UI スレッドからの処理の除去
ここでは、マンデルブロ アプリケーションの UI スレッドから描画処理を除去する方法について説明します。 描画処理を UI スレッドからワーカー スレッドに移動すると、イメージの生成はワーカー スレッドによってバックグラウンドで行われるので、UI スレッドでメッセージを処理できるようになります。
同時実行ランタイムには、タスクを実行する方法として、タスク グループ、非同期エージェント、および軽量タスクの 3 つが用意されています。 どの方法を使用しても UI スレッドから処理を除去できますが、取り消し処理をサポートしているのはタスク グループなので、この例では Concurrency::task_group オブジェクトを使用します。 このチュートリアルの後半では、取り消し処理を使用して、クライアント ウィンドウがサイズ変更されるときに実行される処理の量を減らし、ウィンドウが破棄されるときにクリーンアップを実行します。
また、この例では、UI スレッドとワーカー スレッドが相互に通信できるようにするために Concurrency::unbounded_buffer オブジェクトも使用します。 ワーカー スレッドは、イメージの生成が終了すると、Bitmap オブジェクトへのポインターを unbounded_buffer オブジェクトに送信した後、描画メッセージを UI スレッドにポストします。 UI スレッドは unbounded_buffer オブジェクトから Bitmap オブジェクトを受け取り、それをクライアント ウィンドウに描画します。
描画処理を UI スレッドから除去するには
stdafx.h に次の #include ディレクティブを追加します。
#include <agents.h> #include <ppl.h>
ChildView.h で、task_group および unbounded_buffer メンバー変数を、CChildView クラスの protected セクションに追加します。 task_group オブジェクトは、描画を実行するタスクを保持します。unbounded_buffer オブジェクトは、完成したマンデルブロ イメージを保持します。
Concurrency::task_group m_DrawingTasks; Concurrency::unbounded_buffer<BitmapPtr> m_MandelbrotImages;
ChildView.cpp で、using ディレクティブを Concurrency 名前空間に追加します。
using namespace Concurrency;
CChildView::DrawMandelbrot メソッドで、Bitmap::UnlockBits を呼び出した後に、Concurrency::send 関数を呼び出して Bitmap オブジェクトを UI スレッドに渡します。 その後、描画メッセージを UI スレッドにポストし、クライアント領域を無効にします。
// Unlock the bitmap from system memory. pBitmap->UnlockBits(&bitmapData); // Add the Bitmap object to image queue. send(m_MandelbrotImages, pBitmap); // Post a paint message to the UI thread. PostMessage(WM_PAINT); // Invalidate the client area. InvalidateRect(NULL, FALSE);
更新された Bitmap オブジェクトを受け取ってイメージをクライアント ウィンドウに描画するように、CChildView::OnPaint メソッドを更新します。
void CChildView::OnPaint() { CPaintDC dc(this); // device context for painting // If the unbounded_buffer object contains a Bitmap object, // draw the image to the client area. BitmapPtr pBitmap; if (try_receive(m_MandelbrotImages, pBitmap)) { if (pBitmap != NULL) { // Draw the bitmap to the client area. Graphics g(dc); g.DrawImage(pBitmap.get(), 0, 0); } } // Draw the image on a worker thread if the image is not available. else { RECT rc; GetClientRect(&rc); m_DrawingTasks.run([rc,this]() { DrawMandelbrot(BitmapPtr(new Bitmap(rc.right, rc.bottom))); }); } }
CChildView::OnPaint メソッドは、マンデルブロ イメージがメッセージ バッファーに存在しない場合に、イメージを生成するタスクを作成します。 最初の描画メッセージの場合や、別のウィンドウがクライアント ウィンドウの前面に移動されている場合などは、メッセージ バッファーに Bitmap オブジェクトが含まれません。
アプリケーションをビルドして実行することにより、アプリケーションが正常に更新されたことを確認します。
描画処理がバックグラウンドで実行されるようになったため、UI の応答性が向上しています。
[ページのトップへ]
描画パフォーマンスの向上
マンデルブロ フラクタルの生成は、各ピクセルの計算が他のすべての計算とは無関係に行われるので、並列化に適した候補です。 描画処理を並列化するには、CChildView::DrawMandelbrot メソッドの外側の for ループを、次のように Concurrency::parallel_for アルゴリズムの呼び出しに変更します。
// Compute whether each point lies in the Mandelbrot set.
parallel_for (0u, height, [&](UINT row)
{
// Loop body omitted for brevity.
});
各ビットマップ要素の計算は独立しているので、ビットマップ メモリにアクセスする描画操作を同期する必要はありません。 これにより、使用できるプロセッサの数が増えると、パフォーマンスが向上します。
[ページのトップへ]
取り消し処理のサポートの追加
ここでは、ウィンドウのサイズ変更を処理する方法、およびウィンドウが破棄されるときにアクティブな描画タスクを取り消す方法について説明します。
ドキュメント「PPL における取り消し処理」では、ランタイムで取り消し処理がどのように動作するかについて説明します。 取り消し処理は他の処理と連携して行われます。このため、すぐに実行される訳ではありません。 取り消されたタスクを停止するため、ランタイムは、そのタスクからランタイムへの以降の呼び出しの間に内部例外をスローします。 前のセクションでは、parallel_for アルゴリズムを使用して描画タスクのパフォーマンスを向上させる方法について説明しました。 ランタイムは parallel_for の呼び出しによってタスクの停止を有効にし、その結果取り消し処理の動作を有効にします。
アクティブなタスクの取り消し
マンデルブロ アプリケーションは、クライアント ウィンドウのサイズと等しい寸法の Bitmap オブジェクトを作成します。 クライアント ウィンドウのサイズが変更されるたびに、アプリケーションは新しいバックグラウンド タスクを作成して、新しいウィンドウ サイズのイメージを生成します。 アプリケーションでは、このような中間的なイメージは必要ありません。最終的なウィンドウ サイズのイメージのみが必要です。 アプリケーションがこの余分な処理を実行しなくて済むように、WM_SIZE メッセージおよび WM_SIZING メッセージのメッセージ ハンドラーでアクティブな描画タスクを取り消し、ウィンドウのサイズが変更された後で描画処理を再スケジュールできます。
ウィンドウのサイズが変更されたときにアクティブな描画タスクをアプリケーションで取り消すには、WM_SIZING メッセージおよび WM_SIZE メッセージのハンドラーの中で、Concurrency::task_group::cancel メソッドを呼び出します。 WM_SIZE メッセージのハンドラーは、Concurrency::task_group::wait メソッドも呼び出して、すべてのアクティブなタスクが完了するのを待ってから、更新されたウィンドウ サイズでの描画タスクを再スケジュールします。
クライアント ウィンドウが破棄されるときは、アクティブな描画タスクをすべて取り消すことをお勧めします。 アクティブな描画タスクをすべて取り消すことで、クライアント ウィンドウが破棄された後でワーカー スレッドが UI スレッドにメッセージをポストしないことが保証されます。 アプリケーションは、WM_DESTROY メッセージのハンドラー内でアクティブな描画タスクをすべて取り消します。
取り消しへの応答
描画タスクを実行する CChildView::DrawMandelbrot メソッドは、取り消しに応答する必要があります。 ランタイムは例外処理を使用してタスクを取り消すので、CChildView::DrawMandelbrot メソッドは例外セーフな方法を使用して、すべてのリソースが正しくクリーンアップされることを保証する必要があります。 この例では、Resource Acquisition Is Initialization (RAII) パターンを使用して、タスクを取り消すときにビットマップのビットがロックされていないことを保証します。
マンデルブロ アプリケーションに取り消し処理のサポートを追加するには
ChildView.h で、CChildView クラスの protected セクションに、OnSize、OnSizing、および OnDestroy のメッセージ マップ関数の宣言を追加します。
afx_msg void OnPaint(); afx_msg void OnSize(UINT, int, int); afx_msg void OnSizing(UINT, LPRECT); afx_msg void OnDestroy(); DECLARE_MESSAGE_MAP()
ChildView.cpp で、WM_SIZE、WM_SIZING、および WM_DESTROY メッセージのハンドラーを含むようにメッセージ マップを変更します。
BEGIN_MESSAGE_MAP(CChildView, CWnd) ON_WM_PAINT() ON_WM_SIZE() ON_WM_SIZING() ON_WM_DESTROY() END_MESSAGE_MAP()
CChildView::OnSizing メソッドを実装します。 このメソッドは既存の描画タスクをすべて取り消します。
void CChildView::OnSizing(UINT nSide, LPRECT lpRect) { // The window size is changing; cancel any existing drawing tasks. m_DrawingTasks.cancel(); }
CChildView::OnSize メソッドを実装します。 このメソッドは、既存の描画タスクをすべて取り消し、更新されたクライアント ウィンドウ サイズに対する新しい描画タスクを作成します。
void CChildView::OnSize(UINT nType, int cx, int cy) { // The window size has changed; cancel any existing drawing tasks. m_DrawingTasks.cancel(); // Wait for any existing tasks to finish. m_DrawingTasks.wait(); // If the new size is non-zero, create a task to draw the Mandelbrot // image on a separate thread. if (cx != 0 && cy != 0) { m_DrawingTasks.run([cx,cy,this]() { DrawMandelbrot(BitmapPtr(new Bitmap(cx, cy))); }); } }
CChildView::OnDestroy メソッドを実装します。 このメソッドは既存の描画タスクをすべて取り消します。
void CChildView::OnDestroy() { // The window is being destroyed; cancel any existing drawing tasks. m_DrawingTasks.cancel(); // Wait for any existing tasks to finish. m_DrawingTasks.wait(); }
ChildView.cpp で、RAII パターンを実装する scope_guard クラスを定義します。
// Implements the Resource Acquisition Is Initialization (RAII) pattern // by calling the specified function after leaving scope. class scope_guard { public: explicit scope_guard(std::function<void()> f) : m_f(std::move(f)) { } // Dismisses the action. void dismiss() { m_f = nullptr; } ~scope_guard() { // Call the function. if (m_f) { try { m_f(); } catch (...) { terminate(); } } } private: // The function to call when leaving scope. std::function<void()> m_f; // Hide copy constructor and assignment operator. scope_guard(const scope_guard&); scope_guard& operator=(const scope_guard&); };
CChildView::DrawMandelbrot メソッドで、Bitmap::LockBits の呼び出しの後に次のコードを追加します。
// Create a scope_guard object that unlocks the bitmap bits when it // leaves scope. This ensures that the bitmap is properly handled // when the task is canceled. scope_guard guard([&pBitmap, &bitmapData] { // Unlock the bitmap from system memory. pBitmap->UnlockBits(&bitmapData); });
このコードは、scope_guard オブジェクトを作成することで取り消しを処理します。 オブジェクトがスコープから外れたら、ビットマップのビットをロック解除します。
CChildView::DrawMandelbrot メソッドの最後を変更し、ビットマップのビットがロック解除された後で、メッセージが UI スレッドに送信される前に、scope_guard オブジェクトを終了します。 これにより、ビットマップのビットがロック解除される前に UI スレッドが更新されることがなくなります。
// Unlock the bitmap from system memory. pBitmap->UnlockBits(&bitmapData); // Dismiss the scope guard because the bitmap has been // properly unlocked. guard.dismiss(); // Add the Bitmap object to image queue. send(m_MandelbrotImages, pBitmap); // Post a paint message to the UI thread. PostMessage(WM_PAINT); // Invalidate the client area. InvalidateRect(NULL, FALSE);
アプリケーションをビルドして実行することにより、アプリケーションが正常に更新されたことを確認します。
ウィンドウのサイズを変更すると、最終的なウィンドウ サイズに対してのみ描画処理が実行されます。 ウィンドウが破棄されると、アクティブな描画タスクもすべて取り消されます。
[ページのトップへ]