Поделиться через


Пошаговое руководство. Удаление задач из потоков пользовательского интерфейса

В этом документе показано, как использовать среду выполнения с параллелизмом для перемещения работ, выполняемых потоком пользовательского интерфейса в приложении Microsoft Foundation Classes (MFC), в рабочий поток. Также в документе показано, как улучшить производительность продолжительной операции по отрисовке.

Удаление выполняемой работы из потока пользовательского интерфейса путем выгрузки блокирующих операций, например отрисовки, в рабочий поток, может уменьшить время отклика приложения. В этом пошаговом руководстве в качестве примера длительной блокирующей операции используется процедура отрисовки фрактала Мандельброта. Отрисовка фрактала Мандельброта также является хорошим кандидатом для параллелизации, поскольку вычисление каждого пикселя не зависит от других вычислений.

Обязательные компоненты

Прежде чем начать выполнение этого пошагового руководства, необходимо ознакомиться со следующими разделами.

Также перед освоением этого пошагового руководства рекомендуется разобраться в основах разработки приложений MFC и GDI+. Дополнительные сведения о MFC см. в разделе Приложения MFC для рабочего стола. Дополнительные сведения о GDI+ см. в разделе GDI+.

Подразделы

Это пошаговое руководство содержит следующие подразделы.

  • Создание MFC приложения

  • Реализация версии приложения, последовательно рассчитывающей фрактал Мандельброта

  • Удаление задач из потоков пользовательского интерфейса

  • Повышение производительности отрисовки

  • Добавление поддержки отмены

Создание MFC приложения

В этом разделе описывается, как создать простое приложение MFC.

Создание нового приложения Visual C++ MFC

  1. В меню Файл последовательно выберите пункты Создать и Проект.

  2. В области Установленные шаблоны диалогового окна Новый проект выберите Visual C++, а затем в области Шаблоны выберите MFC Application. Введите имя проекта, например Мандельброт, затем нажмите кнопку ОК, чтобы отобразить Мастер приложений MFC.

  3. На панели Тип приложения выберите Один документ. Убедитесь, что флажок Поддержка архитектуры Document/View снят.

  4. Нажмите Готово, чтобы создать проект и закрыть Мастер приложений MFC.

    Убедитесь, что приложение было успешно создано, выполнив его построение и запуск. Для построения приложения в меню Построение выберите команду Построить решение. Если построение приложения выполнено успешно, запустите приложение, нажав кнопку Начать отладку в меню Отладка.

Реализация версии приложения, последовательно рассчитывающей фрактал Мандельброта

В этом разделе описано, как выполнить отрисовку фрактала Мандельброта. Эта версия отрисовывает фрактал Мандельброта в виде объекта GDI+ Bitmap и затем копирует содержимое этого растрового изображения в клиентское окно.

Реализация версии приложения, последовательно рассчитывающей фрактал Мандельброта

  1. Добавьте в файл 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 добавьте директиву using в пространство имен Gdiplus.

    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. Данный метод отрисовывает фрактал Мандельброта в виде объекта типа 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. Убедитесь, что приложение было успешно обновлено, выполнив его построение и запуск.

На следующем рисунке показаны результаты выполнения приложения "Мандельброт".

Приложение "Мандельброт"

Поскольку вычисление каждого пикселя требует больших затрат вычислительной мощности, поток пользовательского интерфейса не способен обрабатывать дополнительные сообщения до окончания всего вычисления. Это может уменьшить скорость ответа приложения. Однако можно уменьшить значение этой проблемы, удалив вычисления из потока пользовательского интерфейса.

[Наверх]

Удаление вычислений из потока пользовательского интерфейса

В этом разделе показано, как удалить вычисления по отрисовке из потока пользовательского интерфейса приложения "Мандельброт" В результате перемещения вычислений по отрисовке из потока пользовательского интерфейса в рабочий поток, поток пользовательского интерфейса получает возможность обрабатывать сообщения, в то время как рабочий поток создает изображение в фоновом режиме.

Среда выполнения с параллелизмом предоставляет три способа запуска задач: группы задач, асинхронные агенты и упрощенные задачи. Несмотря на то, что для удаления работы из потока пользовательского интерфейса можно использовать любой из этих механизмов, в данном примере используется объект Concurrency::task_group, поскольку группы задач поддерживают отмену. Позже в данном пошаговом руководстве отмена будет использоваться для уменьшения количества вычислений, производимых при изменении размера окна, и для выполнения очистки при уничтожении окна.

Также в этом примере используется объект Concurrency::unbounded_buffer для обеспечения связи между потоком пользовательского интерфейса и рабочим потоком. После того, как рабочий поток сгенерировал изображение, он посылает указатель на объект Bitmap объекту unbounded_buffer, а затем отправляет сообщение изображения в поток пользовательского интерфейса. Поток пользовательского интерфейса получает от объекта unbounded_buffer объект Bitmap и отрисовывает его в клиентском окне.

Удаление вычислений по отрисовке из потока пользовательского интерфейса

  1. Добавьте в файл stdafx.h следующие директивы #include.

    #include <agents.h>
    #include <ppl.h>
    
  2. В файле ChildView.h добавьте переменные-члены task_group and unbounded_buffer в раздел protected класса CChildView. Объект task_group содержит задачи, выполняющие отрисовку; объект unbounded_buffer содержит готовое изображение фрактала Мандельброта.

    concurrency::task_group m_DrawingTasks;
    concurrency::unbounded_buffer<BitmapPtr> m_MandelbrotImages;
    
  3. В файле ChildView.cpp добавьте директиву using в пространство имен concurrency.

    using namespace concurrency;
    
  4. В методе CChildView::DrawMandelbrot, после вызова метода Bitmap::UnlockBits, вызовите функцию Concurrency::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);
    
  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)));
          });
       }
    }
    

    Метод CChildView::OnPaint создает задачу по созданию изображения фрактала Мандельброта, если его нет в буфере сообщений. Буфер сообщений не содержит объект Bitmap в случае, например, когда сообщение изображения является первым или другое окно выведено поверх клиентского окна.

  6. Убедитесь, что приложение было успешно обновлено, выполнив его построение и запуск.

Теперь время отклика пользовательского интерфейса уменьшилось, поскольку вычисления по отрисовке выполняются в фоновом режиме.

[Наверх]

Повышение производительности отрисовки

Отрисовка фрактала Мандельброта — является хороший кандидат для параллелизации, поскольку вычисление каждого пикселя не зависит от других вычислений. Для параллелизации процесса отрисовки, преобразуйте внешний цикл 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.
});

Поскольку вычисления каждого элемента растрового изображения не зависят друг от друга, синхронизировать операции по отрисовке, обращающиеся к памяти, в которой хранится растровое изображение, не нужно. Это улучшает производительность пропорционально увеличению количества доступных процессоров.

[Наверх]

Добавление поддержки отмены

Этот раздел описывает обработку изменения размера окна и процедуру отмены любой активной задачи по отрисовке при уничтожении окна.

Документ Отмена в библиотеке параллельных шаблонов объясняет, как работает отмена в среде выполнения. Отмена выполняется совместно, следовательно, не может выполняться мгновенно. Для остановки отмененной задачи среда выполнения порождает исключение в ходе последующего вызова из задачи в среду выполнения. В предыдущем разделе показано, как использовать алгоритм parallel_for для повышения производительности задачи по отрисовке изображения. Вызов объекта parallel_for позволяет среде выполнения остановить задачу, тем самым позволяя выполняться отмене.

Отмена активных задач

Приложение "Мандельброт" создает объекты Bitmap, размер которых совпадает с размером клиентского окна. Каждый раз, когда клиентское окно изменяет размер, приложение создает фоновую задачу создания изображения для нового размера окна. Приложению не требуются промежуточные изображения; только изображение конечного размера окна. Чтобы предотвратить выполнение дополнительной работы приложением, можно отменить любые активные задачи по отрисовке в обработчике сообщений для сообщений WM_SIZE и WM_SIZING, а затем запланировать задачу заново, когда размер окна будет изменен.

Для отмены активных задач при изменении размера окна приложение вызывает метод Concurrency::task_group::cancel обработчика для сообщений WM_SIZING и WM_SIZE. Обработчик для сообщения WM_SIZE также вызывает метод Concurrency::task_group::wait, который ожидает завершения всех активных задач, а затем планирует новую задачу отрисовки для обновленного размера окна.

Хорошей практикой является отмена любых активных задач по отрисовке после уничтожения клиентского окна. Отмена любых активных задач по отрисовке позволяет быть уверенным, что рабочий поток не будет отсылать сообщения в поток пользовательского интерфейса после уничтожения клиентского окна. Приложение отменяет любые активные задачи по отрисовке в обработчике сообщения WM_DESTROY.

Реакция на отмену

Метод CChildView::DrawMandelbrot, выполняющий задачу по отрисовке, должен отреагировать на отмену. Поскольку среда выполнения использует обработку исключений для отмены задач, метод CChildView::DrawMandelbrot должен использовать механизм, безопасный для исключений, чтобы гарантировать корректную очистку всех ресурсов. Данный пример использует шаблон Получение ресурса есть инициализация (RAII), чтобы гарантировать, что после отмены задачи все биты растрового изображения будут разблокированы.

Добавление поддержки отмены в приложение "Мандельброт"

  1. В файле ChildView.h в раздел protected класса CChildView добавьте объявления для функций 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()
    
  2. Измените схему сообщения в файле 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()
    
  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. Добавьте следующий код в метод 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. Когда объект покидает область, он разблокирует биты растрового изображения.

  8. Измените окончание метода 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);
    
  9. Убедитесь, что приложение было успешно обновлено, выполнив его построение и запуск.

При изменении размеров окна вычисления по отрисовке выполняются только для окончательного размера окна. Любая активная задача по отрисовке отменяется при уничтожении окна.

[Наверх]

См. также

Основные понятия

Параллелизм задач (среда выполнения с параллелизмом)

Асинхронные блоки сообщений

Функции передачи сообщений

Параллельные алгоритмы

Отмена в библиотеке параллельных шаблонов

Другие ресурсы

Пошаговые руководства по среде выполнения с параллелизмом

Приложения MFC для рабочего стола