Instruções passo a passo: removendo trabalho de um thread de interface de usuário

Este documento demonstra como usar o Runtime de Simultaneidade para mover o trabalho executado pelo thread de interface do usuário em um aplicativo da MFC para um thread de trabalho. Este documento também demonstra como aprimorar o desempenho de uma longa operação de desenho.

Remover o trabalho do thread da IU ao descarregar operações de bloqueio (por exemplo, desenho) para threads de trabalho pode aprimorar a capacidade de resposta do aplicativo. Este passo a passo usa uma rotina de desenho que gera o fractal Mandelbrot para demonstrar uma longa operação de bloqueio. A geração do fractal Mandelbrot também é uma boa candidata para paralelização porque a computação de cada pixel é independente de todas as outras computações.

Pré-requisitos

Leia os seguintes tópicos antes de iniciar este passo a passo:

Também recomendamos que você entenda as noções básicas do desenvolvimento de aplicativos da MFD e do GDI+ antes de iniciar este passo a passo. Para obter mais informações, confira Aplicativos de área de trabalho da MFC. Para obter mais informações sobre GDI+, confira GDI+.

Seções

Este passo a passo contém as seguintes seções:

Criar um aplicativo da MFC

Esta seção descreve como criar o aplicativo básico da MFC.

Criar um aplicativo da MFC no Visual C++

  1. Use o Assistente para Aplicativo do MFC para criar um aplicativo MFC com todas as configurações padrão. Confira Guia passo a passo: usar os novos controles de shell do MFC para instruções sobre como abrir o assistente em sua versão do Visual Studio.

  2. Digite um nome para o projeto, por exemplo, Mandelbrot, e clique em OK para exibir o Assistente de Aplicativo da MFC.

  3. No painel Tipo de Aplicativo, selecione Documento único. Verifique se a caixa de seleção Suporte à arquitetura de documento/exibição está desmarcada.

  4. Clique em Concluir para criar o projeto e fechar o Assistente de Aplicativo da MFC.

    Verifique se o aplicativo foi criado com êxito compilando e executando-o. Para compilar o aplicativo, no menu Compilar, clique em Compilar Solução. Se o aplicativo for compilado com êxito, execute-o clicando em Iniciar Depuração no menu Depurar.

Implementar a versão serial do aplicativo Mandelbrot

Esta seção descreve como desenhar o fractal Mandelbrot. Esta versão desenha o fractal Mandelbrot para um objeto Bitmap GDI+ e copia o conteúdo desse bitmap na janela do cliente.

Implementar a versão serial do aplicativo Mandelbrot

  1. Em pch.h (stdafx.h no Visual Studio 2017 e anteriores), adicione a seguinte diretiva #include:

    #include <memory>
    
  2. Em ChildView.h, após a diretiva pragma, defina o tipo BitmapPtr. O tipo BitmapPtr permite que um ponteiro para um objeto Bitmap seja compartilhado por vários componentes. O objeto Bitmap é excluído quando não é mais referenciado por nenhum componente.

    typedef std::shared_ptr<Gdiplus::Bitmap> BitmapPtr;
    
  3. Em ChildView.h, adicione o seguinte código à seção protected da classe CChildView:

    protected:
       // Draws the Mandelbrot fractal to the specified Bitmap object.
       void DrawMandelbrot(BitmapPtr);
    
    protected:
       ULONG_PTR m_gdiplusToken;
    
  4. Em ChildView.cpp, comente ou remova as linhas a seguir.

    //#ifdef _DEBUG
    //#define new DEBUG_NEW
    //#endif
    

    Em builds de depuração, essa etapa impede que o aplicativo use o alocador DEBUG_NEW, que é incompatível com GDI+.

  5. Em ChildView.cpp, adicione uma diretiva using ao namespace Gdiplus.

    using namespace Gdiplus;
    
  6. Adicione o código a seguir ao construtor e ao destruidor da classe CChildView para inicializar e desligar o GDI+.

    CChildView::CChildView()
    {
       // Initialize GDI+.
       GdiplusStartupInput gdiplusStartupInput;
       GdiplusStartup(&m_gdiplusToken, &gdiplusStartupInput, NULL);
    }
    
    CChildView::~CChildView()
    {
       // Shutdown GDI+.
       GdiplusShutdown(m_gdiplusToken);
    }
    
  7. Implementar o método de CChildView::DrawMandelbrot . Esse método desenha o fractal Mandelbrot para o objeto Bitmap especificado.

    // 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. Implementar o método de CChildView::OnPaint . Esse método chama CChildView::DrawMandelbrot e copia o conteúdo do objeto Bitmap na janela.

    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. Verifique se o aplicativo foi atualizado com êxito compilando e executando-o.

A ilustração a seguir mostra os resultados do aplicativo Mandelbrot.

The Mandelbrot Application.

Como a computação para cada pixel é computacionalmente cara, o thread da IU não pode processar mensagens adicionais até que a computação geral seja concluída. Isso pode diminuir a capacidade de resposta no aplicativo. No entanto, você pode aliviar esse problema removendo o trabalho do thread da IU.

[Parte superior]

Remover o trabalho do thread da IU

Esta seção mostra como remover o trabalho de desenho do thread da IU no aplicativo Mandelbrot. Ao mover o trabalho de desenho do thread da IU para um thread de trabalho, o thread da IU poderá processar mensagens à medida que o thread de trabalho gera a imagem em segundo plano.

O Runtime de Simultaneidade fornece três maneiras de executar tarefas: grupos de tarefas, agentes assíncronos e tarefas leves. Embora você possa usar qualquer um desses mecanismos para remover o trabalho do thread da IU, este exemplo usa um objeto concurrency::task_group porque os grupos de tarefas dão suporte ao cancelamento. Este passo a passo usa o cancelamento posteriormente para reduzir a quantidade de trabalho que é executada quando a janela do cliente é redimensionada e para executar a limpeza quando a janela é destruída.

Este exemplo também usa um objeto concurrency::unbounded_buffer para habilitar o thread da IU e o thread de trabalho a se comunicarem. Depois que o thread de trabalho produz a imagem, ele envia um ponteiro para o objeto Bitmap e para o objeto unbounded_buffer e, em seguida, posta uma mensagem de pintura no thread da IU. O thread da IU recebe do objeto unbounded_buffer o objeto Bitmap e o desenha para a janela do cliente.

Remover o trabalho de desenho do thread da IU

  1. Em pch.h (stdafx.h no Visual Studio 2017 e anteriores), adicione as seguintes diretivas #include:

    #include <agents.h>
    #include <ppl.h>
    
  2. Em ChildView.h, adicione as variáveis de membro task_group e unbounded_buffer à seção protected da classe CChildView. O objeto task_group contém as tarefas que executam o desenho. O objeto unbounded_buffer contém a imagem Mandelbrot concluída.

    concurrency::task_group m_DrawingTasks;
    concurrency::unbounded_buffer<BitmapPtr> m_MandelbrotImages;
    
  3. Em ChildView.cpp, adicione uma diretiva using ao namespace concurrency.

    using namespace concurrency;
    
  4. No método CChildView::DrawMandelbrot, após a chamada a Bitmap::UnlockBits, chame a função concurrency::send para passar o objeto Bitmap para o thread da IU. Em seguida, poste uma mensagem de pintura no thread da IU e invalide a área de cliente.

    // 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. Atualize o método CChildView::OnPaint para receber o objeto Bitmap atualizado e desenhe a imagem na janela do cliente.

    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)));
          });
       }
    }
    

    O método CChildView::OnPaint cria uma tarefa para gerar a imagem Mandelbrot se não existir uma no buffer de mensagens. O buffer de mensagem não conterá um objeto Bitmap em casos como a mensagem de pintura inicial e quando outra janela for movida na frente da janela do cliente.

  6. Verifique se o aplicativo foi atualizado com êxito compilando e executando-o.

A interface do usuário agora é mais responsiva porque o trabalho de desenho é executado em segundo plano.

[Parte superior]

Aprimorar o desempenho do desenho

A geração do fractal Mandelbrot é uma boa candidata para paralelização porque a computação de cada pixel é independente de todas as outras computações. Para paralelizar o procedimento de desenho, converta o loop externo for no método CChildView::DrawMandelbrot em uma chamada para o algoritmo concurrency::parallel_for da seguinte maneira.

// Compute whether each point lies in the Mandelbrot set.
parallel_for (0u, height, [&](UINT row)
{
   // Loop body omitted for brevity.
});

Como a computação de cada elemento bitmap é independente, você não precisa sincronizar as operações de desenho que acessam a memória do bitmap. Isso permite que o desempenho seja dimensionado à medida que o número de processadores disponíveis aumenta.

[Parte superior]

Adicionar suporte ao cancelamento

Esta seção descreve como lidar com o redimensionamento de janelas e como cancelar as tarefas de desenho ativas quando a janela é destruída.

O documento Cancelamento no PPL explica como o cancelamento funciona no runtime. O cancelamento é cooperativo, portanto, isso não ocorre imediatamente. Para interromper uma tarefa cancelada, o runtime gera uma exceção interna durante uma chamada subsequente da tarefa no runtime. A seção anterior mostra como usar o algoritmo parallel_for para aprimorar o desempenho da tarefa de desenho. A chamada para parallel_for permite que o runtime interrompa a tarefa e, portanto, permite que o cancelamento funcione.

Cancelar tarefas ativas

O aplicativo Mandelbrot cria objetos Bitmap cujas dimensões correspondem ao tamanho da janela do cliente. Sempre que a janela do cliente é redimensionada, o aplicativo cria uma tarefa em segundo plano adicional para gerar uma imagem para o novo tamanho da janela. O aplicativo não requer essas imagens intermediárias. Ele requer apenas a imagem para o tamanho final da janela. Para impedir que o aplicativo execute esse trabalho adicional, você pode cancelar todas as tarefas de desenho ativas nos manipuladores de mensagens para as mensagens WM_SIZE e WM_SIZING e, em seguida, reagendar o trabalho de desenho depois que a janela for redimensionada.

Para cancelar tarefas de desenho ativas quando a janela é redimensionada, o aplicativo chama o método concurrency::task_group::cancel nos manipuladores para as mensagens WM_SIZING e WM_SIZE. O manipulador da mensagem WM_SIZE também chama o método concurrency::task_group::wait para aguardar a conclusão de todas as tarefas ativas e, em seguida, reagenda a tarefa de desenho para o tamanho da janela atualizada.

Quando a janela do cliente é destruída, é uma boa prática cancelar todas as tarefas de desenho ativas. Cancelar tarefas de desenho ativas garante que os threads de trabalho não postem mensagens no thread da IU depois que a janela do cliente é destruída. O aplicativo cancela todas as tarefas de desenho ativas no manipulador para a mensagem WM_DESTROY.

Responder ao cancelamento

O método CChildView::DrawMandelbrot, que executa a tarefa de desenho, deve responder ao cancelamento. Como o runtime usa tratamento de exceção para cancelar tarefas, o método CChildView::DrawMandelbrot deve usar um mecanismo de segurança de exceção para garantir que todos os recursos sejam limpos corretamente. Este exemplo usa o padrão RAII (Resource Acquisition Is Initialization) para garantir que os bits de bitmap sejam desbloqueados quando a tarefa é cancelada.

Adicionar suporte para cancelamento no aplicativo Mandelbrot
  1. Em ChildView.h, na seção protected da classe CChildView, adicione declarações para as funções de mapa de mensagens OnSize, OnSizing e 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. Em ChildView.cpp, modifique o mapa de mensagens para conter manipuladores para as mensagens WM_SIZE, WM_SIZING e WM_DESTROY.

    BEGIN_MESSAGE_MAP(CChildView, CWnd)
       ON_WM_PAINT()
       ON_WM_SIZE()
       ON_WM_SIZING()
       ON_WM_DESTROY()
    END_MESSAGE_MAP()
    
  3. Implementar o método de CChildView::OnSizing . Esse método cancela todas as tarefas de desenho existentes.

    void CChildView::OnSizing(UINT nSide, LPRECT lpRect)
    {
       // The window size is changing; cancel any existing drawing tasks.
       m_DrawingTasks.cancel();
    }
    
  4. Implementar o método de CChildView::OnSize . Esse método cancela todas as tarefas de desenho existentes e cria uma tarefa de desenho para o tamanho atualizado da janela do cliente.

    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. Implementar o método de CChildView::OnDestroy . Esse método cancela todas as tarefas de desenho existentes.

    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. Em ChildView.cpp, defina a classe scope_guard, que implementa o padrão 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. Adicione o seguinte código ao método CChildView::DrawMandelbrot após a chamada para 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);      
    });
    

    Esse código manipula o cancelamento criando um objeto scope_guard. Quando o objeto sai do escopo, ele desbloqueia os bits de bitmap.

  8. Modifique o final do método CChildView::DrawMandelbrot para descartar o objeto scope_guard depois que os bits de bitmap forem desbloqueados, mas antes que qualquer mensagem seja enviada para o thread da IU. Isso garante que o thread da IU não seja atualizado antes que os bits de bitmap sejam desbloqueados.

    // 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. Verifique se o aplicativo foi atualizado com êxito compilando e executando-o.

Quando você redimensiona a janela, o trabalho de desenho é executado somente para o tamanho final da janela. Todas as tarefas de desenho ativas também são canceladas quando a janela é destruída.

[Parte superior]

Confira também

Instruções passo a passo do runtime de simultaneidade
Paralelismo de tarefas
Blocos de mensagens assíncronos
Funções de transmissão de mensagem
Algoritmos paralelos
Cancelamento no PPL
Aplicativos da área de trabalho MFC