从 UI 线程中移除工作
本部分介绍了如何从 Mandelbrot 应用程序中的 UI 线程中移除绘制工作。 通过将绘制工作从 UI 线程移动到工作线程,UI 线程可以在工作线程在后台生成图像的同时处理消息。
并发运行时提供三种方法来运行任务:任务组、异步代理和轻量级任务。 尽管可以使用其中任何一种机制从 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 中,将 task_group 和 unbounded_buffer 成员变量添加到
// CChildView 类的 protected 部分。 task_group 对象保存执行绘制的任务;
// unbounded_buffer 对象保存已完成的 Mandelbrot 图像。
concurrency::task_group m_DrawingTasks;
concurrency::unbounded_buffer<BitmapPtr> m_MandelbrotImages;
// 3. 在 ChildView.cpp 中,向 concurrency 命名空间添加一个 using 指令。
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)));
});
}
}
// 6. 如果消息缓冲区中不存在 Mandelbrot 图像,CChildView::OnPaint 方法将创建一个任务来生成
// Mandelbrot 图像。 在初次画图消息以及另一个窗口移动到了客户端窗口的前面等情况下,
// 消息缓冲区不会包含 Bitmap 对象。
// 通过生成并运行应用程序来验证其是否已成功更新。
UI 现在响应速度更快,因为绘制工作在后台执行。
提高绘制性能
Mandelbrot 分形的生成非常适合并行化,因为每个像素的计算都是独立于所有其他计算的。 若要并行化绘制过程,请将 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.
});
由于每个位图元素的计算都是独立的,因此无需同步访问位图内存的绘制操作。 这使得性能可以随着可用处理器数的增加而扩展。
添加对取消的支持
本部分介绍了如何处理窗口大小调整,以及如何在销毁窗口时取消任何活动的绘制任务。
文档 PPL 中的取消说明了取消在运行时中的工作原理。 取消是合作性的;因此,它不会立即发生。 若要停止已取消的任务,运行时会在从任务到运行时的后续调用期间引发内部异常。 上一部分介绍了如何使用 parallel_for 算法来提高绘制任务的性能。 调用 parallel_for 能够使运行时停止任务,从而使取消能够发挥作用。