演练:从用户界面线程中移除工作
本文档演示如何使用并发运行时将 Microsoft 基础类 (MFC) 应用程序中的用户界面 (UI) 线程所执行的工作移到辅助线程。本文档还演示如何提高长时间绘制操作的性能。
通过将阻塞操作(例如绘制操作)卸载到辅助线程,从 UI 线程中移除工作,可以提高应用程序的响应能力。本演练使用可生成 Mandelbrot 分形的绘制例程来演示长时间的阻塞操作。生成 Mandelbrot 分形也很适合于并行化,因为每个像素的计算都独立于所有其他计算。
系统必备
在开始本演练之前,请阅读下列主题:
此外,建议您在开始本演练之前,先了解 MFC 应用程序开发和 GDI+ 的基础知识。有关 MFC 的更多信息,请参见 MFC 桌面应用程序。有关 GDI+ 的更多信息,请参见 GDI+。
各节内容
本演练包含以下各节:
创建 MFC 应用程序
实现 Mandelbrot 应用程序的序列化版本
从用户界面线程中移除工作
提高绘制性能
添加取消支持
创建 MFC 应用程序
本节描述如何创建基本 MFC 应用程序。
创建 Visual C++ MFC 应用程序
在**“文件”菜单上,单击“新建”,然后单击“项目”**。
在**“新建项目”对话框的“已安装的模板”窗格中,选择“Visual C++”,然后在“模板”窗格中选择“MFC 应用程序”。为项目键入名称(例如 Mandelbrot),然后单击“确定”以显示“MFC 应用程序向导”**。
在**“应用程序类型”窗格中选择“单个文档”。确保清除“文档/视图结构支持”**复选框。
单击**“完成”以创建项目,然后关闭“MFC 应用程序向导”**。
通过生成并运行应用程序来验证该应用程序已成功创建。若要生成应用程序,请在**“生成”菜单上单击“生成解决方案”。如果成功生成了该应用程序,请单击“调试”菜单上的“开始调试”**来运行该应用程序。
实现 Mandelbrot 应用程序的序列化版本
本节说明如何绘制 Mandelbrot 分形。此版本向 GDI+ Bitmap 对象绘制 Mandelbrot 分形,然后将此位图的内容复制到客户端窗口中。
实现 Mandelbrot 应用程序的序列化版本
在 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
在调试版本中,此步骤可阻止应用程序使用与 GDI+ 不兼容的 DEBUG_NEW 分配器。
在 ChildView.cpp 中,将 using 指令添加到 Gdiplus 命名空间。
using namespace Gdiplus;
将以下代码添加到 CChildView 类的构造函数和析构函数中以初始化并关闭 GDI+。
CChildView::CChildView() { // Initialize GDI+. GdiplusStartupInput gdiplusStartupInput; GdiplusStartup(&m_gdiplusToken, &gdiplusStartupInput, NULL); } CChildView::~CChildView() { // Shutdown GDI+. GdiplusShutdown(m_gdiplusToken); }
实现 CChildView::DrawMandelbrot 方法。此方法向指定的 Bitmap 对象绘制 Mandelbrot 分形。
// 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); } }
通过生成并运行应用程序来验证应用程序是否已成功更新。
下图演示 Mandelbrot 应用程序的结果。
由于计算每个像素要消耗大量的计算资源,因此在整体计算完成之前,UI 线程无法处理其他消息。这会降低应用程序的响应能力。但是,您可以通过从 UI 线程中移除工作来缓解此问题。
Top
从 UI 线程中移除工作
本节演示如何从 Mandelbrot 应用程序的 UI 线程中移除绘制工作。将绘制工作从 UI 线程移动到辅助线程后,辅助线程在后台生成图像,同时 UI 线程可以处理消息。
并发运行时提供三种运行任务的方式:任务组、异步代理和轻量级任务。虽然您可以使用任何一种机制从 UI 线程中移除工作,此示例使用 concurrency::task_group 对象,因为任务组支持取消。本演练稍后将使用取消功能,以便在调整客户端窗口大小时减少所执行的工作量,并在窗口销毁时执行清理工作。
此示例还使用了 concurrency::unbounded_buffer 对象,使 UI 线程和辅助线程以相互进行通信。辅助线程在产生图像后将一个指向 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 对象保留已完成的 Mandelbrot 图像。
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);
更新 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 对象。
通过生成并运行应用程序来验证应用程序是否已成功更新。
现在,UI 线程能够更快地响应,因为绘制工作在后台执行。
Top
提高绘制性能
生成 Mandelbrot 分形很适合于并行化,因为每个像素的计算都独立于所有其他计算。并行化绘图的过程,将外部for循环中CChildView::DrawMandelbrot方法的调用 concurrency::parallel_for 算法,如下所示。
// Compute whether each point lies in the Mandelbrot set.
parallel_for (0u, height, [&](UINT row)
{
// Loop body omitted for brevity.
});
因为每个位图元素的计算都是独立的,所以您不必同步访问位图内存的绘制操作。这样,性能将随可用处理器数的增加而进行调整。
Top
添加取消支持
本节描述如何调整窗口大小,以及如何在窗口销毁时取消任何活动的绘制任务。
PPL 中的取消操作文档解释取消操作如何在运行时中工作。取消是协作性的操作;因此,它不会立即发生。若要停止已取消的任务,则在从任务中对运行时进行后续调用期间,运行时将引发内部异常。上一节演示了如何使用 parallel_for 算法提高绘制任务的性能。调用 parallel_for 后运行时将能够停止任务,并因此使取消操作起作用。
取消活动任务
Mandelbrot 应用程序将创建其维度与客户端窗口大小匹配的 Bitmap 对象。每次调整客户端窗口大小时,此应用程序都会另外创建一个后台任务,以便针对新的窗口大小生成图像。此应用程序不需要这些中间图像,而只需要针对最终窗口大小的图像。若要阻止此应用程序执行这一附加工作,您可以取消 WM_SIZE 和 WM_SIZING 消息的消息处理程序中的任何活动的绘制任务,然后在调整窗口大小之后重新计划绘制工作。
若要取消活动绘图的任务,调整窗口大小时,应用程序调用 concurrency::task_group::cancel 中的处理程序方法WM_SIZING和WM_SIZE消息。该处理程序的WM_SIZE消息还调用 concurrency::task_group::wait 等待所有活动完成的任务,然后重新安排的更新的窗口大小的绘图任务的方法。
当客户端窗口销毁时,最好取消任何活动的绘制任务。取消任何活动的绘制任务可确保在客户端窗口销毁后,辅助线程不会向 UI 线程发送消息。此应用程序将取消 WM_DESTROY 消息的处理程序中的任何活动的绘制任务。
响应取消操作
执行绘制任务的 CChildView::DrawMandelbrot 方法必须对取消操作做出响应。因为运行时使用异常处理来取消任务,所以 CChildView::DrawMandelbrot 方法必须使用异常安全机制来保证正确清理所有资源。此示例使用“获取资源即初始化”(RAII) 模式来保证在取消任务时不锁定位图位。
在 Mandelbrot 应用程序中添加取消支持
在 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&); };
在调用 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 对象处理取消操作。当该对象离开范围时,它将取消锁定位图位。
修改 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);
通过生成并运行应用程序来验证应用程序是否已成功更新。
当调整窗口的大小时,仅针对最终窗口大小执行绘制工作。当窗口销毁时,也将取消任何活动的绘制任务。
Top