Пошаговое руководство. Удаление задач из потоков пользовательского интерфейса
В этом документе показано, как использовать среду выполнения параллелизма для перемещения работы, выполняемой потоком пользовательского интерфейса (UI) в приложении Microsoft Foundation Classes (MFC) в рабочий поток. В этом документе также показано, как повысить производительность длительной операции рисования.
Удаление работы из потока пользовательского интерфейса путем разгрузки блокирующих операций, например рисования, в рабочие потоки может повысить скорость реагирования приложения. В этом пошаговом руководстве используется подпрограмма рисования, которая создает fractal Mandelbrot для демонстрации длительной операции блокировки. Создание fractal Mandelbrot также является хорошим кандидатом на параллелизацию, так как вычисления каждого пикселя не зависят от всех других вычислений.
Необходимые компоненты
Ознакомьтесь со следующими разделами перед началом работы с этим пошаговом руководстве.
Мы также рекомендуем понять основы разработки приложений MFC и GDI+ перед началом этого пошагового руководства. Дополнительные сведения о MFC см. в разделе "Классические приложения MFC". Дополнительные сведения о GDI+см. в разделе GDI+.
Разделы
Это пошаговое руководство содержит следующие разделы:
Создание приложения MFC
В этом разделе описывается создание базового приложения MFC.
Создание приложения MFC Visual C++
Используйте мастер приложений MFC для создания приложения MFC со всеми параметрами по умолчанию. См . пошаговое руководство. Использование новых элементов управления оболочки MFC для инструкций по открытию мастера для вашей версии Visual Studio.
Введите имя проекта, например, и нажмите кнопку "ОК",
Mandelbrot
чтобы отобразить мастер приложений MFC.В области "Тип приложения" выберите один документ. Убедитесь, что флажок " Поддержка архитектуры документа и представления" снят.
Нажмите кнопку "Готово", чтобы создать проект и закрыть мастер приложений MFC.
Убедитесь, что приложение было успешно создано путем создания и запуска его. Чтобы создать приложение, в меню "Сборка" нажмите кнопку "Создать решение". Если приложение успешно строится, запустите приложение, нажав кнопку "Начать отладку " в меню отладки .
Реализация последовательной версии приложения Mandelbrot
В этом разделе описывается, как нарисовать fractal Mandelbrot. Эта версия рисует fractal Mandelbrot в объект GDI+ Bitmap, а затем копирует содержимое этого растрового изображения в окно клиента.
Реализация последовательной версии приложения Mandelbrot
В pch.h (stdafx.h в Visual Studio 2017 и более ранних версиях) добавьте следующую
#include
директиву:#include <memory>
В ChildView.h после
pragma
директивы определитеBitmapPtr
тип. ТипBitmapPtr
позволяет указателю наBitmap
объект совместно использовать несколько компонентов. ОбъектBitmap
удаляется, если он больше не ссылается на любой компонент.typedef std::shared_ptr<Gdiplus::Bitmap> BitmapPtr;
В ChildView.h добавьте следующий код в
protected
разделCChildView
класса: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
В сборках отладки этот шаг запрещает приложению использовать
DEBUG_NEW
распределитель, несовместимый с GDI+.В 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
. Этот метод рисует fractal 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); }
Реализуйте метод
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.
Так как вычисления для каждого пикселя являются вычислительными затратами, поток пользовательского интерфейса не может обрабатывать дополнительные сообщения, пока общее вычисление не завершится. Это может снизить скорость отклика в приложении. Однако эту проблему можно устранить, удалив работу из потока пользовательского интерфейса.
[В начало]
Удаление работы из потока пользовательского интерфейса
В этом разделе показано, как удалить рисунок из потока пользовательского интерфейса в приложении Mandelbrot. Переместив рисование из потока пользовательского интерфейса в рабочий поток, поток пользовательского интерфейса может обрабатывать сообщения, так как рабочий поток создает изображение в фоновом режиме.
Среда выполнения параллелизма предоставляет три способа выполнения задач: групп задач, асинхронных агентов и упрощенных задач. Хотя для удаления работы из потока пользовательского интерфейса можно использовать любой из этих механизмов, в этом примере используется объект параллелизма::task_group , так как группы задач поддерживают отмену. В этом пошаговом руководстве позже используется отмена для уменьшения объема работы, выполняемой при изменении размера окна клиента, и для выполнения очистки при уничтожении окна.
В этом примере также используется объект параллелизма::unbounded_buffer , чтобы включить поток пользовательского интерфейса и рабочий поток для взаимодействия друг с другом. После того как рабочий поток создает изображение, он отправляет указатель Bitmap
на объект unbounded_buffer
объекту, а затем отправляет сообщение о краске в поток пользовательского интерфейса. Затем поток пользовательского интерфейса получает от unbounded_buffer
объекта Bitmap
объект и рисует его в окне клиента.
Удаление работы рисования из потока пользовательского интерфейса
В pch.h (stdafx.h в Visual Studio 2017 и более ранних версиях) добавьте следующие
#include
директивы:#include <agents.h> #include <ppl.h>
В ChildView.h добавьте
task_group
иunbounded_buffer
членные переменные вprotected
разделCChildView
класса. Объектtask_group
содержит задачи, выполняющие рисование;unbounded_buffer
объект содержит завершенное изображение Мандельброта.concurrency::task_group m_DrawingTasks; concurrency::unbounded_buffer<BitmapPtr> m_MandelbrotImages;
В ChildView.cpp добавьте директиву
using
вconcurrency
пространство имен.using namespace concurrency;
В методе
CChildView::DrawMandelbrot
после вызоваBitmap::UnlockBits
вызовите функцию параллелизма::send , чтобы передатьBitmap
объект в поток пользовательского интерфейса. Затем опубликуйте сообщение с краской в поток пользовательского интерфейса и опустите клиентская область.// 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))); }); } }
Метод
CChildView::OnPaint
создает задачу для создания образа Mandelbrot, если он не существует в буфере сообщений. Буфер сообщений не будет содержатьBitmap
объект в таких случаях, как исходное сообщение с краской, а также при перемещении другого окна перед окном клиента.Убедитесь, что приложение было успешно обновлено, создав и запустив его.
Пользовательский интерфейс теперь более адаптивн, так как работа рисования выполняется в фоновом режиме.
[В начало]
Повышение производительности рисования
Создание fractal Mandelbrot является хорошим кандидатом на параллелизацию, так как вычисления каждого пикселя не зависят от всех других вычислений. Чтобы параллелизировать процедуру рисования, преобразуйте внешний for
цикл в CChildView::DrawMandelbrot
метод в вызов параллелизма::p arallel_for algorithm, как показано ниже.
// 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_SIZE
сообщений и WM_SIZING
сообщений, а затем перепланировать работу рисования после изменения размера окна.
Чтобы отменить активные задачи рисования при изменении размера окна, приложение вызывает метод параллелизма::task_group::cancel в обработчиках для WM_SIZING
сообщений и WM_SIZE
сообщений. Обработчик WM_SIZE
сообщения также вызывает параллелизм::task_group::wait , чтобы ждать завершения всех активных задач, а затем перепланирует задачу рисования для обновленного размера окна.
При уничтожении окна клиента рекомендуется отменить все активные задачи рисования. Отмена любых активных задач рисования гарантирует, что рабочие потоки не публикуют сообщения в поток пользовательского интерфейса после уничтожения окна клиента. Приложение отменяет все активные задачи рисования в обработчике WM_DESTROY
сообщения.
Реагирование на отмену
Метод CChildView::DrawMandelbrot
, выполняющий задачу рисования, должен реагировать на отмену. Так как среда выполнения использует обработку исключений для отмены задач, метод должен использовать механизм, безопасный для исключений, CChildView::DrawMandelbrot
чтобы гарантировать правильность очистки всех ресурсов. В этом примере используется шаблон инициализации ресурсов (RAII), чтобы гарантировать разблокировку битов растрового изображения при отмене задачи.
Добавление поддержки отмены в приложении Mandelbrot
В ChildView.h в
protected
разделеCChildView
класса добавьте объявления дляOnSizing
OnSize
функций сопоставления сообщений и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 определите
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&); };
Добавьте следующий код в
CChildView::DrawMandelbrot
метод после вызова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); });
Этот код обрабатывает отмену путем создания
scope_guard
объекта. Когда объект покидает область, он разблокирует битовые изображения.Измените конец
CChildView::DrawMandelbrot
метода, чтобы закрытьscope_guard
объект после разблокировки битов растрового изображения, но перед отправкой сообщений в поток пользовательского интерфейса. Это гарантирует, что поток пользовательского интерфейса не обновляется до разблокировки битовых изображений.// 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);
Убедитесь, что приложение было успешно обновлено, создав и запустив его.
При изменении размера окна работа рисования выполняется только для окончательного размера окна. Все активные задачи рисования также отменяются при уничтожении окна.
[В начало]
См. также
Пошаговые руководства по среде выполнения с параллелизмом
Параллелизм задач
Асинхронные блоки сообщений
Функции передачи сообщений
Параллельные алгоритмы
Отмена в библиотеке параллельных шаблонов
Приложения MFC для рабочего стола