分享方式:


逐步解說:從使用者介面執行緒中移除工作

本檔示範如何使用並行執行時間,將 Microsoft Foundation Classes (MFC) 應用程式中使用者介面 (UI) 執行緒所執行的工作移至背景工作執行緒。 本檔也會示範如何改善冗長繪圖作業的效能。

藉由卸載封鎖作業來從 UI 執行緒移除工作,例如繪製到背景工作執行緒,可以改善應用程式的回應性。 本逐步解說會使用產生 Mandelbrot 分形的繪圖常式來示範冗長的封鎖作業。 Mandelbrot 分形的產生也是平行化的良好候選項目,因為每個圖元的計算與所有其他計算無關。

必要條件

開始本逐步解說之前,請先閱讀下列主題:

我們也建議您先瞭解 MFC 應用程式開發和 GDI+ 的基本概念,再開始本逐步解說。 如需 MFC 的詳細資訊,請參閱 MFC 傳統型應用程式 。 如需 GDI+的詳細資訊,請參閱 GDI+

區段

本逐步解說包含下列各節:

建立 MFC 應用程式

本節說明如何建立基本的 MFC 應用程式。

建立 Visual C++ MFC 應用程式

  1. 使用 MFC 應用程式精靈 來建立具有所有預設設定的 MFC 應用程式。 如需如何開啟 Visual Studio 版本的精靈的指示,請參閱 逐步解說:使用新的 MFC 殼層控制項

  2. 輸入專案的名稱,例如 Mandelbrot ,然後按一下 [ 確定 ] 以顯示 MFC 應用程式精靈

  3. 在 [ 應用程式類型] 窗格中,選取 [單一檔 ]。 確定 已清除 [檔/檢視架構支援 ] 核取方塊。

  4. 按一下 [完成 ] 以建立專案並關閉 [MFC 應用程式精靈 ]。

    藉由建置並執行應用程式,確認應用程式已成功建立。 若要建置應用程式,請在 [ 置] 功能表上,按一下 [建置方案 ]。 如果應用程式建置成功,請按一下 [偵錯] 功能表上的 [開始 偵錯] 來執行應用程式。

實作 Mandelbrot 應用程式的序列版本

本節說明如何繪製 Mandelbrot 分形。 此版本會將 Mandelbrot 分形繪製至 GDI+ Bitmap 物件,然後將該點陣圖 的內容複寫到用戶端視窗。

實作 Mandelbrot 應用程式的序列版本

  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 中,將下列程式碼新增至 protected 類別的 CChildView 區段:

    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 中,將 指示詞新增 usingGdiplus 命名空間。

    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 方法。 這個方法會將 Mandelbrot 分形繪製至指定的 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. 藉由建置並執行應用程式,確認應用程式已成功更新。

下圖顯示 Mandelbrot 應用程式的結果。

The Mandelbrot Application.

由於每個圖元的計算成本很高,所以 UI 執行緒在整體計算完成之前,無法處理其他訊息。 這可能會降低應用程式中的回應性。 不過,您可以從 UI 執行緒移除工作,以減輕此問題。

[靠上]

從 UI 執行緒移除工作

本節說明如何從 Mandelbrot 應用程式中的 UI 執行緒中移除繪圖工作。 藉由將繪圖工作從 UI 執行緒移至背景工作執行緒,UI 執行緒就可以處理訊息,因為背景工作執行緒會產生影像。

並行執行時間提供三種方式來執行工作:工作組、非同步代理程式和 輕量型工作 雖然您可以使用下列任一機制從 UI 執行緒中移除工作,但此範例會使用 並行::task_group 物件,因為工作組支援取消。 本逐步解說稍後會使用取消來減少用戶端視窗調整大小時所執行的工作量,以及在終結視窗時執行清除。

此範例也會使用 並行::unbounded_buffer 物件,讓 UI 執行緒和背景工作執行緒彼此通訊。 背景工作執行緒產生映射之後,會將物件的指標 Bitmap 傳送至 unbounded_buffer 物件,然後將繪製訊息張貼至 UI 執行緒。 然後 UI 執行緒會從 物件接收物件 Bitmapunbounded_buffer 並將它繪製至用戶端視窗。

從 UI 執行緒移除繪圖工作

  1. pch.h 中( Visual Studio 2017 和更早版本中的 stdafx.h ),新增下列 #include 指示詞:

    #include <agents.h>
    #include <ppl.h>
    
  2. 在 ChildView.h 中,將 和 unbounded_buffer 成員變數新增 task_groupprotected 類別的 CChildView 區段。 物件 task_group 會保存執行繪圖的工作; unbounded_buffer 物件會保存已完成的 Mandelbrot 影像。

    concurrency::task_group m_DrawingTasks;
    concurrency::unbounded_buffer<BitmapPtr> m_MandelbrotImages;
    
  3. 在 ChildView.cpp 中,將 指示詞新增 usingconcurrency 命名空間。

    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. CChildView::OnPaint更新 方法以接收更新 Bitmap 的物件,並將影像繪製至用戶端視窗。

    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)));
          });
       }
    }
    

    如果訊息緩衝區中沒有 Mandelbrot 影像,方法 CChildView::OnPaint 會建立工作來產生 Mandelbrot 影像。 訊息緩衝區不會包含 Bitmap 物件,例如初始繪製訊息,以及在用戶端視窗前面移動另一個視窗時。

  6. 藉由建置並執行應用程式,確認應用程式已成功更新。

UI 現在回應較快,因為繪圖工作是在背景中執行。

[靠上]

改善繪圖效能

Mandelbrot 分形的產生是平行化的良好候選項目,因為每個圖元的計算與所有其他計算無關。 若要平行處理繪圖程式,請將 方法中的 CChildView::DrawMandelbrot 外部 for 迴圈轉換成對 concurrency::p arallel_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 可讓執行時間停止工作,因此可讓取消運作。

取消作用中工作

Mandelbrot 應用程式會 Bitmap 建立物件,其維度符合用戶端視窗的大小。 每次調整用戶端視窗大小時,應用程式都會建立額外的背景工作,以產生新視窗大小的影像。 應用程式不需要這些中繼映射;它只需要最終視窗大小的影像。 若要防止應用程式執行此額外工作,您可以在 和 WM_SIZING 訊息的訊息處理常式 WM_SIZE 中取消任何使用中的繪圖工作,然後在視窗調整大小之後重新排程繪圖工作。

若要在視窗調整大小時取消使用中繪圖工作,應用程式會在 和 WM_SIZE 訊息的 WM_SIZING 處理常式中呼叫 concurrency::task_group::cancel 方法。 訊息的 WM_SIZE 處理常式也會呼叫 concurrency::task_group::wait 方法,等候所有使用中工作完成,然後重新排程已更新視窗大小的繪圖工作。

當用戶端視窗終結時,最好取消任何使用中的繪圖工作。 取消任何作用中的繪圖工作,可確保背景工作執行緒不會在用戶端視窗終結之後,將訊息張貼到 UI 執行緒。 應用程式會取消訊息處理常式 WM_DESTROY 中的任何使用中繪圖工作。

回應取消

執行 CChildView::DrawMandelbrot 繪圖工作的方法必須回應取消。 由於執行時間會使用例外狀況處理來取消工作, CChildView::DrawMandelbrot 因此 方法必須使用例外狀況安全的機制,以確保所有資源都已正確清除。 此範例使用 資源擷取 為初始化 (RAII) 模式,以確保取消工作時,點陣圖位會解除鎖定。

在 Mandelbrot 應用程式中新增取消的支援
  1. 在 ChildView.h 的 protected 類別區 CChildView 段中,新增 、 OnSizingOnDestroy 訊息對應函式的 OnSize 宣告。

    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_SIZINGWM_DESTROY 訊息的 WM_SIZE 處理常式。

    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 中,定義 類別 scope_guard ,以實作 RAII 模式。

    // 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. 在 呼叫 Bitmap::LockBits 之後, CChildView::DrawMandelbrot 將下列程式碼新增至 方法:

    // 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. 修改 方法的 CChildView::DrawMandelbrot 結尾,以在點陣圖位解除鎖定之後關閉 scope_guard 物件,但在任何訊息傳送至 UI 執行緒之前。 這可確保在解除鎖定點陣圖位之前,不會更新 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 傳統型應用程式