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++
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.
Digite um nome para o projeto, por exemplo,
Mandelbrot
, e clique em OK para exibir o Assistente de Aplicativo da MFC.No painel Tipo de Aplicativo, selecione Documento único. Verifique se a caixa de seleção Suporte à arquitetura de documento/exibição está desmarcada.
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
Em pch.h (stdafx.h no Visual Studio 2017 e anteriores), adicione a seguinte diretiva
#include
:#include <memory>
Em ChildView.h, após a diretiva
pragma
, defina o tipoBitmapPtr
. O tipoBitmapPtr
permite que um ponteiro para um objetoBitmap
seja compartilhado por vários componentes. O objetoBitmap
é excluído quando não é mais referenciado por nenhum componente.typedef std::shared_ptr<Gdiplus::Bitmap> BitmapPtr;
Em ChildView.h, adicione o seguinte código à seção
protected
da classeCChildView
:protected: // Draws the Mandelbrot fractal to the specified Bitmap object. void DrawMandelbrot(BitmapPtr); protected: ULONG_PTR m_gdiplusToken;
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+.Em ChildView.cpp, adicione uma diretiva
using
ao namespaceGdiplus
.using namespace Gdiplus;
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); }
Implementar o método de
CChildView::DrawMandelbrot
. Esse método desenha o fractal Mandelbrot para o objetoBitmap
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); }
Implementar o método de
CChildView::OnPaint
. Esse método chamaCChildView::DrawMandelbrot
e copia o conteúdo do objetoBitmap
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); } }
Verifique se o aplicativo foi atualizado com êxito compilando e executando-o.
A ilustração a seguir mostra os resultados do aplicativo Mandelbrot.
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.
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
Em pch.h (stdafx.h no Visual Studio 2017 e anteriores), adicione as seguintes diretivas
#include
:#include <agents.h> #include <ppl.h>
Em ChildView.h, adicione as variáveis de membro
task_group
eunbounded_buffer
à seçãoprotected
da classeCChildView
. O objetotask_group
contém as tarefas que executam o desenho. O objetounbounded_buffer
contém a imagem Mandelbrot concluída.concurrency::task_group m_DrawingTasks; concurrency::unbounded_buffer<BitmapPtr> m_MandelbrotImages;
Em ChildView.cpp, adicione uma diretiva
using
ao namespaceconcurrency
.using namespace concurrency;
No método
CChildView::DrawMandelbrot
, após a chamada aBitmap::UnlockBits
, chame a função concurrency::send para passar o objetoBitmap
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);
Atualize o método
CChildView::OnPaint
para receber o objetoBitmap
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 objetoBitmap
em casos como a mensagem de pintura inicial e quando outra janela for movida na frente da janela do cliente.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.
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.
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
Em ChildView.h, na seção
protected
da classeCChildView
, adicione declarações para as funções de mapa de mensagensOnSize
,OnSizing
eOnDestroy
.afx_msg void OnPaint(); afx_msg void OnSize(UINT, int, int); afx_msg void OnSizing(UINT, LPRECT); afx_msg void OnDestroy(); DECLARE_MESSAGE_MAP()
Em ChildView.cpp, modifique o mapa de mensagens para conter manipuladores para as mensagens
WM_SIZE
,WM_SIZING
eWM_DESTROY
.BEGIN_MESSAGE_MAP(CChildView, CWnd) ON_WM_PAINT() ON_WM_SIZE() ON_WM_SIZING() ON_WM_DESTROY() END_MESSAGE_MAP()
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(); }
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))); }); } }
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(); }
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&); };
Adicione o seguinte código ao método
CChildView::DrawMandelbrot
após a chamada paraBitmap::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.Modifique o final do método
CChildView::DrawMandelbrot
para descartar o objetoscope_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);
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.
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