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

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

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

Необходимые компоненты

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

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

Разделы

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

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

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

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

  1. Используйте мастер приложений MFC для создания приложения MFC со всеми параметрами по умолчанию. См . пошаговое руководство. Использование новых элементов управления оболочки MFC для инструкций по открытию мастера для вашей версии Visual Studio.

  2. Введите имя проекта, например, и нажмите кнопку "ОК", Mandelbrotчтобы отобразить мастер приложений MFC.

  3. В области "Тип приложения" выберите один документ. Убедитесь, что архитектура документа и представления поддерживает проверка поле с очисткой.

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

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

Реализация последовательной версии приложения Mandelbrot

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

Реализация последовательной версии приложения Mandelbrot

  1. В pch.h (stdafx.h в Visual Studio 2017 и более ранних версиях) добавьте следующую #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. Этот метод рисует 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);
    }
    
  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. Убедитесь, что приложение было успешно обновлено, создав и запустив его.

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

The Mandelbrot Application.

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

[В начало]

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

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

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

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

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

  1. В pch.h (stdafx.h в Visual Studio 2017 и более ранних версиях) добавьте следующие #include директивы:

    #include <agents.h>
    #include <ppl.h>
    
  2. В ChildView.h добавьте task_group и 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вызовите функцию параллелизма::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 создает задачу для создания образа Mandelbrot, если он не существует в буфере сообщений. Буфер сообщений не будет содержать Bitmap объект в таких случаях, как исходное сообщение с краской, а также при перемещении другого окна перед окном клиента.

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

Пользовательский интерфейс теперь более адаптивн, так как работа рисования выполняется в фоновом режиме.

[В начало]

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

Создание 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
  1. В ChildView.h в protected разделе CChildView класса добавьте объявления для OnSizingOnSizeфункций сопоставления сообщений и 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 для рабочего стола