チュートリアル: ユーザー インターフェイス スレッドからの処理の除去

このドキュメントでは、同時実行ランタイムを使用して、Microsoft Foundation Classes (MFC) アプリケーションのユーザー インターフェイス (UI) スレッドによって実行される作業をワーカー スレッドに移動する方法について説明します。 このドキュメントでは、時間のかかる描画操作のパフォーマンスを向上させる方法についても説明します。

ワーカースレッドに対してブロッキング操作 (描画など) をオフロードすることによって UI スレッドから作業を削除すると、アプリケーションの応答性が向上します。 このチュートリアルでは、マンデルブロ フラクタルを生成する描画ルーチンを使用して、時間のかかるブロッキング操作を示します。 また、マンデルブロ フラクタルの生成も、並列処理の候補として適しています。これは、各ピクセルの計算が他のすべての計算から独立しているためです。

前提条件

このチュートリアルを開始する前に、次のトピックを参照してください。

また、このチュートリアルを開始する前に、MFC アプリケーションの開発と GDI+ の基本を理解しておくことをお勧めします。 詳細については、「MFC デスクトップ アプリケーション」を参照してください。 GDI+ の詳細については、「GDI+」を参照してください。

セクション

このチュートリアルは、次のセクションで構成されています。

MFC アプリケーションの作成

ここでは、基本的な MFC アプリケーションを作成する方法について説明します。

Visual C++ MFC アプリケーションを作成するには

  1. MFC アプリケーション ウィザードを使用して、すべての既定の設定を含む MFC アプリケーションを作成します。 お使いのバージョンの Visual Studio のウィザードを開く方法については、「チュートリアル: 新しい MFC シェル コントロールの使用」を参照してください。

  2. プロジェクトの名前 (Mandelbrot など) を入力し、[OK] をクリックして、MFC アプリケーション ウィザードを表示します。

  3. [アプリケーションの種類] ペインで、[単一ドキュメント] を選択します。 [ドキュメント/ビューアーキテクチャのサポート] チェック ボックスがオフになっていることを確認します。

  4. [完了] をクリックしてプロジェクトを作成し、MFC アプリケーション ウィザードを閉じます。

    アプリケーションをビルドして実行することにより、アプリケーションが正常に作成されたことを確認します。 アプリケーションをビルドするには、[ビルド] メニューの [ソリューションのビルド] をクリックします。 アプリケーションが正常にビルドされたら、[デバッグ] メニューの [デバッグの開始] をクリックして、アプリケーションを実行します。

マンデルブロ アプリケーションのシリアル バージョンの実装

このセクションでは、マンデルブロ フラクタルを描画する方法について説明します。 このバージョンでは、マンデルブロ フラクタルを GDI+ Bitmap オブジェクトに描画し、そのビットマップの内容をクライアント ウィンドウにコピーします。

マンデルブロ アプリケーションのシリアル バージョンの実装

  1. pch.h (Visual Studio 2017 以前の場合は stdafx.h) で、次の #include ディレクティブを追加します:

    #include <memory>
    
  2. ChildView. h で、pragma ディレクティブの 後に BitmapPtr 型を定義 します。 BitmapPtr 型を使用すると、Bitmap オブジェクトへ のポインターを複数のコンポーネントで共有できます。 Bitmap オブジェクトは、コンポーネントによって参照されなくなったときに削除されます。

    typedef std::shared_ptr<Gdiplus::Bitmap> BitmapPtr;
    
  3. ChildView. h で、次のコードを CChildView クラスの protected セクションに追加します:

    protected:
       // Draws the Mandelbrot fractal to the specified Bitmap object.
       void DrawMandelbrot(BitmapPtr);
    
    protected:
       ULONG_PTR m_gdiplusToken;
    
  4. ChildView .cpp で、次の行をコメントアウトするか削除します。

    //#ifdef _DEBUG
    //#define new DEBUG_NEW
    //#endif
    

    デバッグ ビルドでは、この手順により、アプリケーションで DEBUG_NEW アロケーターを使用できなくなります。このアロケーターは、GDI+ と互換性がありません。

  5. ChildView .cpp で、using ディレクティブを Gdiplus 名前空間に追加します。

    using namespace Gdiplus;
    
  6. 次のコードを CChildView クラスのコンストラクターおよびデストラクターに追加して、GDI+ を初期化してシャットダウンします。

    CChildView::CChildView()
    {
       // Initialize GDI+.
       GdiplusStartupInput gdiplusStartupInput;
       GdiplusStartup(&m_gdiplusToken, &gdiplusStartupInput, NULL);
    }
    
    CChildView::~CChildView()
    {
       // Shutdown GDI+.
       GdiplusShutdown(m_gdiplusToken);
    }
    
  7. 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);
    }
    
  8. 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);
       }
    }
    
  9. アプリケーションをビルドして実行することにより、アプリケーションが正常に作成されたことを確認します。

次の図は、マンデルブロ アプリケーションの結果を示しています。

The Mandelbrot Application.

各ピクセルの計算は計算に負荷がかかるため、全体的な計算が終了するまで、UI スレッドは追加のメッセージを処理できません。 これにより、アプリケーションの応答性が低下する可能性があります。 ただし、UI スレッドから作業を削除することで、この問題を軽減できます。

[トップ]

UI スレッドからの作業の削除

このセクションでは、マンデルブロ アプリケーションの UI スレッドから描画作業を削除する方法について説明します。 描画作業を UI スレッドからワーカースレッドに移動すると、ワーカースレッドがバックグラウンドでイメージを生成するときに、UI スレッドがメッセージを処理できます。

同時実行ランタイムには、タスク グループ非同期エージェント、および軽量タスクの 3 つのタスク実行方法が用意されています。 これらのメカニズムのいずれかを使用して UI スレッドから作業を削除できますが、この例では、タスク グループがキャンセルをサポートしているため、concurrency:: task_group オブジェクトを使用しています。 このチュートリアルでは、後でキャンセルを使用して、クライアント ウィンドウのサイズ変更時に実行される作業量を減らし、ウィンドウが破棄されたときにクリーンアップを実行します。

また、この例では、 concurrency:: unbounded_buffer オブジェクトを使用して、UI スレッドとワーカースレッドが相互に通信できるようにします。 ワーカースレッドはイメージを生成した後、Bitmap オブジェクトへのポインターを unbounded_buffer オブジェクトに送信し、描画メッセージを UI スレッドにポストします。 その後、UI スレッドが unbounded_buffer オブジェクトから Bitmap オブジェクトを受け取り、それをクライアント ウィンドウに描画します。

UI スレッドから描画作業を削除するには

  1. pch.h (Visual Studio 2017 以前の場合は stdafx.h) で、次の #include ディレクティブを追加します:

    #include <agents.h>
    #include <ppl.h>
    
  2. ChildView .h で、CChildView クラスの protected セクションに task_groupunbounded_buffer のメンバー変数を追加します。 task_group オブジェクトは描画を実行するタスクを保持し、unbounded_buffer オブジェクトは完成したマンデルブロ イメージを保持します。

    concurrency::task_group m_DrawingTasks;
    concurrency::unbounded_buffer<BitmapPtr> m_MandelbrotImages;
    
  3. ChildView .cpp で、using ディレクティブを concurrency 名前空間に追加します。

    using namespace concurrency;
    
  4. 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);
    
  5. 更新された 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 オブジェクトが含まれません。

  6. アプリケーションをビルドして実行することにより、アプリケーションが正常に作成されたことを確認します。

描画作業がバックグラウンドで実行されるため、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.
});

各 bitmap 要素の計算は独立しているため、ビットマップ メモリにアクセスする描画操作を同期する必要はありません。 これにより、使用可能なプロセッサの数が増えるにつれて、パフォーマンスを向上させることができます。

[トップ]

キャンセルのサポートを追加する

このセクションでは、ウィンドウのサイズ変更を処理する方法と、ウィンドウが破棄されたときにアクティブな描画タスクをキャンセルする方法について説明します。

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) パターンを使用して、タスクが取り消されたときにビットマップ ビットのロックが解除されることを保証します。

マンデルブロ アプリケーションでキャンセルのサポートを追加するには
  1. ChildView.h の CChildView クラスの protected セクションで、OnSizeOnSizing および 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()
    
  2. ChildView .cpp で、メッセージマップを変更して、 WM_SIZEWM_SIZING および WM_DESTROY の各メッセージのハンドラーを含めます。

    BEGIN_MESSAGE_MAP(CChildView, CWnd)
       ON_WM_PAINT()
       ON_WM_SIZE()
       ON_WM_SIZING()
       ON_WM_DESTROY()
    END_MESSAGE_MAP()
    
  3. CChildView::OnSizing メソッドを実装します。 このメソッドは、既存の描画タスクをキャンセルします。

    void CChildView::OnSizing(UINT nSide, LPRECT lpRect)
    {
       // The window size is changing; cancel any existing drawing tasks.
       m_DrawingTasks.cancel();
    }
    
  4. 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)));
          });
       }
    }
    
  5. 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();
    }
    
  6. 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&);
    };
    
  7. 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 オブジェクトを作成することによって取り消し処理を行います。 オブジェクトがスコープから出ると、ビットマップ ビットのロックが解除されます。

  8. ビットマップ ビットのロックが解除された後、UI スレッドにメッセージが送信される前に、 scope_guard オブジェクトを破棄するように CChildView::DrawMandelbrot メソッドの末尾を変更します。 これにより、ビットマップ ビットのロックが解除される前に 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);
    
  9. アプリケーションをビルドして実行することにより、アプリケーションが正常に作成されたことを確認します。

ウィンドウのサイズを変更すると、描画作業は、最終的なウィンドウ サイズに対してのみ実行されます。 アクティブな描画タスクも、ウィンドウが破棄されたときにキャンセルされます。

[トップ]

関連項目

コンカレンシー ランタイムのチュートリアル
タスクの並列処理
非同期メッセージ ブロック
メッセージ パッシング関数
並列アルゴリズム
PPL における取り消し処理
MFC デスクトップ アプリケーション