Walkthrough: Removing Work from a User-Interface Thread

This document demonstrates how to use the Concurrency Runtime to move the work that is performed by the user-interface (UI) thread in a Microsoft Foundation Classes (MFC) application to a worker thread. This document also demonstrates how to improve the performance of a lengthy drawing operation.

Removing work from the UI thread by offloading blocking operations, for example, drawing, to worker threads can improve the responsiveness of your application. This walkthrough uses a drawing routine that generates the Mandelbrot fractal to demonstrate a lengthy blocking operation. The generation of the Mandelbrot fractal is also a good candidate for parallelization because the computation of each pixel is independent of all other computations.

Prerequisites

Read the following topics before you start this walkthrough:

We also recommend that you understand the basics of MFC application development and GDI+ before you start this walkthrough. For more information about MFC, see MFC Reference. For more information about GDI+, see GDI+.

Sections

This walkthrough contains the following sections:

  • Creating the MFC Application

  • Implementing the Serial Version of the Mandelbrot Application

  • Removing Work from the User-Interface Thread

  • Improving Drawing Performance

  • Adding Support for Cancellation

Creating the MFC Application

This section describes how to create the basic MFC application.

To create a Visual C++ MFC application

  1. On the File menu, click New, and then click Project.

  2. In the New Project dialog box, in the Installed Templates pane, select Visual C++, and then, in the Templates pane, select MFC Application. Type a name for the project, for example, Mandelbrot, and then click OK to display the MFC Application Wizard.

  3. In the Application Type pane, select Single document. Ensure that the Document/View architecture support check box is cleared.

  4. Click Finish to create the project and close the MFC Application Wizard.

    Verify that the application was created successfully by building and running it. To build the application, on the Build menu, click Build Solution. If the application builds successfully, run the application by clicking Start Debugging on the Debug menu.

Implementing the Serial Version of the Mandelbrot Application

This section describes how to draw the Mandelbrot fractal. This version draws the Mandelbrot fractal to a GDI+ Bitmap object and then copies the contents of that bitmap to the client window.

To implement the serial version of the Mandelbrot application

  1. In stdafx.h, add the following #include directive:

    #include <memory>
    
  2. In ChildView.h, after the pragma directive, define the BitmapPtr type. The BitmapPtr type enables a pointer to a Bitmap object to be shared by multiple components. The Bitmap object is deleted when it is no longer referenced by any component.

    typedef std::shared_ptr<Gdiplus::Bitmap> BitmapPtr;
    
  3. In ChildView.h, add the following code to the protected section of the CChildView class:

    protected:
       // Draws the Mandelbrot fractal to the specified Bitmap object.
       void DrawMandelbrot(BitmapPtr);
    
    protected:
       ULONG_PTR m_gdiplusToken;
    
  4. In ChildView.cpp, comment out or remove the following lines.

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

    In Debug builds, this step prevents the application from using the DEBUG_NEW allocator, which is incompatible with GDI+.

  5. In ChildView.cpp, add a using directive to the Gdiplus namespace.

    using namespace Gdiplus;
    
  6. Add the following code to the constructor and destructor of the CChildView class to initialize and shut down GDI+.

    CChildView::CChildView()
    {
       // Initialize GDI+.
       GdiplusStartupInput gdiplusStartupInput;
       GdiplusStartup(&m_gdiplusToken, &gdiplusStartupInput, NULL);
    }
    
    CChildView::~CChildView()
    {
       // Shutdown GDI+.
       GdiplusShutdown(m_gdiplusToken);
    }
    
  7. Implement the CChildView::DrawMandelbrot method. This method draws the Mandelbrot fractal to the specified Bitmap object.

    // 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. Implement the CChildView::OnPaint method. This method calls CChildView::DrawMandelbrot and then copies the contents of the Bitmap object to the window.

    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. Verify that the application was updated successfully by building and running it.

The following illustration shows the results of the Mandelbrot application.

The Mandelbrot Application

Because the computation for each pixel is computationally expensive, the UI thread cannot process additional messages until the overall computation finishes. This could decrease responsiveness in the application. However, you can relieve this problem by removing work from the UI thread.

[go to top]

Removing Work from the UI Thread

This section shows how to remove the drawing work from the UI thread in the Mandelbrot application. By moving drawing work from the UI thread to a worker thread, the UI thread can process messages as the worker thread generates the image in the background.

The Concurrency Runtime provides three ways to run tasks: task groups, asynchronous agents, and lightweight tasks. Although you can use any one of these mechanisms to remove work from the UI thread, this example uses a Concurrency::task_group object because task groups support cancellation. This walkthrough later uses cancellation to reduce the amount of work that is performed when the client window is resized, and to perform cleanup when the window is destroyed.

This example also uses a Concurrency::unbounded_buffer object to enable the UI thread and the worker thread to communicate with each other. After the worker thread produces the image, it sends a pointer to the Bitmap object to the unbounded_buffer object and then posts a paint message to the UI thread. The UI thread then receives from the unbounded_buffer object the Bitmap object and draws it to the client window.

To remove the drawing work from the UI thread

  1. In stdafx.h, add the following #include directives:

    #include <agents.h>
    #include <ppl.h>
    
  2. In ChildView.h, add task_group and unbounded_buffer member variables to the protected section of the CChildView class. The task_group object holds the tasks that perform drawing; the unbounded_buffer object holds the completed Mandelbrot image.

    Concurrency::task_group m_DrawingTasks;
    Concurrency::unbounded_buffer<BitmapPtr> m_MandelbrotImages;
    
  3. In ChildView.cpp, add a using directive to the Concurrency namespace.

    using namespace Concurrency;
    
  4. In the CChildView::DrawMandelbrot method, after the call to Bitmap::UnlockBits, call the Concurrency::send function to pass the Bitmap object to the UI thread. Then post a paint message to the UI thread and invalidate the client area.

    // 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. Update the CChildView::OnPaint method to receive the updated Bitmap object and draw the image to the client window.

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

    The CChildView::OnPaint method creates a task to generate the Mandelbrot image if one does not exist in the message buffer. The message buffer will not contain a Bitmap object in cases such as the initial paint message and when another window is moved in front of the client window.

  6. Verify that the application was updated successfully by building and running it.

The UI is now more responsive because the drawing work is performed in the background.

[go to top]

Improving Drawing Performance

The generation of the Mandelbrot fractal is a good candidate for parallelization because the computation of each pixel is independent of all other computations. To parallelize the drawing procedure, convert the outer for loop in the CChildView::DrawMandelbrot method to a call to the Concurrency::parallel_for algorithm, as follows.

// Compute whether each point lies in the Mandelbrot set.
parallel_for (0u, height, [&](UINT row)
{
   // Loop body omitted for brevity.
});

Because the computation of each bitmap element is independent, you do not have to synchronize the drawing operations that access the bitmap memory. This enables performance to scale as the number of available processors increases.

[go to top]

Adding Support for Cancellation

This section describes how to handle window resizing and how to cancel any active drawing tasks when the window is destroyed.

The document Cancellation in the PPL explains how cancellation works in the runtime. Cancellation is cooperative; therefore, it does not occur immediately. To stop a canceled task, the runtime throws an internal exception during a subsequent call from the task into the runtime. The previous section shows how to use the parallel_for algorithm to improve the performance of the drawing task. The call to parallel_for enables the runtime to stop the task, and therefore enables cancellation to work.

Cancelling Active Tasks

The Mandelbrot application creates Bitmap objects whose dimensions match the size of the client window. Every time the client window is resized, the application creates an additional background task to generate an image for the new window size. The application does not require these intermediate images; it requires only the image for the final window size. To prevent the application from performing this additional work, you can cancel any active drawing tasks in the message handlers for the WM_SIZE and WM_SIZING messages and then reschedule drawing work after the window is resized.

To cancel active drawing tasks when the window is resized, the application calls the Concurrency::task_group::cancel method in the handlers for the WM_SIZING and WM_SIZE messages. The handler for the WM_SIZE message also calls the Concurrency::task_group::wait method to wait for all active tasks to complete and then reschedules the drawing task for the updated window size.

When the client window is destroyed, it is good practice to cancel any active drawing tasks. Canceling any active drawing tasks makes sure that worker threads do not post messages to the UI thread after the client window is destroyed. The application cancels any active drawing tasks in the handler for the WM_DESTROY message.

Responding to Cancellation

The CChildView::DrawMandelbrot method, which performs the drawing task, must respond to cancellation. Because the runtime uses exception handling to cancel tasks, the CChildView::DrawMandelbrot method must use an exception-safe mechanism to guarantee that all resources are correctly cleaned-up. This example uses the Resource Acquisition Is Initialization (RAII) pattern to guarantee that the bitmap bits are unlocked when the task is canceled.

To add support for cancellation in the Mandelbrot application

  1. In ChildView.h, in the protected section of the CChildView class, add declarations for the OnSize, OnSizing, and OnDestroy message map functions.

    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. In ChildView.cpp, modify the message map to contain handlers for the WM_SIZE, WM_SIZING, and WM_DESTROY messages.

    BEGIN_MESSAGE_MAP(CChildView, CWnd)
       ON_WM_PAINT()
       ON_WM_SIZE()
       ON_WM_SIZING()
       ON_WM_DESTROY()
    END_MESSAGE_MAP()
    
  3. Implement the CChildView::OnSizing method. This method cancels any existing drawing tasks.

    void CChildView::OnSizing(UINT nSide, LPRECT lpRect)
    {
       // The window size is changing; cancel any existing drawing tasks.
       m_DrawingTasks.cancel();
    }
    
  4. Implement the CChildView::OnSize method. This method cancels any existing drawing tasks and creates a new drawing task for the updated client window size.

    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. Implement the CChildView::OnDestroy method. This method cancels any existing drawing tasks.

    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. In ChildView.cpp, define the scope_guard class, which implements the RAII pattern.

    // 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. Add the following code to the CChildView::DrawMandelbrot method after the call to 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);      
    });
    

    This code handles cancellation by creating a scope_guard object. When the object leaves scope, it unlocks the bitmap bits.

  8. Modify the end of the CChildView::DrawMandelbrot method to dismiss the scope_guard object after the bitmap bits are unlocked, but before any messages are sent to the UI thread. This ensures that the UI thread is not updated before the bitmap bits are unlocked.

    // 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. Verify that the application was updated successfully by building and running it.

When you resize the window, drawing work is performed only for the final window size. Any active drawing tasks are also canceled when the window is destroyed.

[go to top]

See Also

Concepts

Task Parallelism (Concurrency Runtime)

Asynchronous Message Blocks

Message Passing Functions

Parallel Algorithms

Cancellation in the PPL

Other Resources

Concurrency Runtime Walkthroughs

MFC Reference