共用方式為


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

本檔案示範如何使用並行運行時間,將Microsoft基礎類別 (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 應用程式的結果。

Mandelbrot 應用程式。

由於每個像素的計算成本很高,所以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 傳統型應用程式