Tutorial: Quitar trabajo de un subproceso de la interfaz de usuario

En este documento se muestra cómo usar el Runtime de simultaneidad para mover el trabajo que realiza el subproceso de interfaz de usuario (UI) en una aplicación de Microsoft Foundation Classes (MFC) a un subproceso de trabajo. En este documento también se muestra cómo mejorar el rendimiento de una operación de dibujo prolongada.

Si se elimina trabajo del subproceso de interfaz de usuario descargando las operaciones de bloqueo (por ejemplo, dibujar) en subprocesos de trabajo, se puede mejorar la capacidad de respuesta de la aplicación. En este tutorial se usa una rutina de dibujo que genera el fractal de Mandelbrot para demostrar una operación de bloqueo prolongada. La generación del fractal de Mandelbrot también es una buena candidata para la paralelización, ya que el cálculo de cada píxel es independiente de todos los demás cálculos.

Requisitos previos

Lea los siguientes temas antes de iniciar este tutorial:

También se recomienda comprender los conceptos básicos del desarrollo de aplicaciones MFC y GDI+ antes de iniciar este tutorial. Para obtener más información sobre MFC, consulte Aplicaciones de escritorio de MFC. Para obtener más información sobre GDI+, consulte GDI+.

Secciones

Este tutorial contiene las siguientes secciones:

Creación de la aplicación MFC

En esta sección se describe cómo crear la aplicación MFC básica.

Para crear una aplicación MFC de Visual C++

  1. Usa el Asistente para aplicaciones MFC para crear una aplicación MFC con toda la configuración predeterminada. Consulte Tutorial: Uso de los nuevos controles de shell de MFC para obtener instrucciones sobre cómo abrir el asistente para su versión de Visual Studio.

  2. Escriba un nombre para el proyecto (por ejemplo, Mandelbrot) y haga clic en Aceptar para mostrar el Asistente para aplicaciones MFC.

  3. En el panel Tipo de aplicación, seleccione Documento único. Asegúrese de que la casilla de compatibilidad con la arquitectura documento/vista está desactivada.

  4. Haga clic en Finalizar para crear el proyecto y cerrar el Asistente para aplicaciones MFC.

    Compruebe que la aplicación se creó correctamente; para ello, compílela y ejecútela. Para compilar la aplicación, en el menú Compilar, haga clic en Compilar solución. Si la aplicación se compila correctamente, haga clic en Iniciar depuración en el menú Depurar para ejecutarla.

Implementación de la versión en serie de la aplicación Mandelbrot

En esta sección se describe cómo dibujar el fractal de Mandelbrot. Esta versión dibuja el fractal de Mandelbrot en un objeto Bitmap de GDI+ y, luego, copia el contenido de ese mapa de bits en la ventana del cliente.

Para implementar la versión en serie de la aplicación Mandelbrot

  1. En pch.h (stdafx.h en Visual Studio 2017 y versiones anteriores), agregue la siguiente directiva #include:

    #include <memory>
    
  2. En ChildView.h, después de la directiva pragma, defina el tipo BitmapPtr. El tipo BitmapPtr permite que varios componentes compartan un puntero a un objeto Bitmap. El objeto Bitmap se elimina cuando ya no hace referencia a él ningún componente.

    typedef std::shared_ptr<Gdiplus::Bitmap> BitmapPtr;
    
  3. En ChildView.h, agregue el código siguiente a la sección protected de la clase CChildView:

    protected:
       // Draws the Mandelbrot fractal to the specified Bitmap object.
       void DrawMandelbrot(BitmapPtr);
    
    protected:
       ULONG_PTR m_gdiplusToken;
    
  4. En ChildView.cpp, convierta en comentario las líneas siguientes o quítelas.

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

    En las compilaciones de depuración, este paso impide que la aplicación use el asignador DEBUG_NEW, que no es compatible con GDI+.

  5. En ChildView.cpp, agregue una directiva using al espacio de nombres Gdiplus.

    using namespace Gdiplus;
    
  6. Agregue el código siguiente al constructor y destructor de la clase CChildView para inicializar y apagar GDI+.

    CChildView::CChildView()
    {
       // Initialize GDI+.
       GdiplusStartupInput gdiplusStartupInput;
       GdiplusStartup(&m_gdiplusToken, &gdiplusStartupInput, NULL);
    }
    
    CChildView::~CChildView()
    {
       // Shutdown GDI+.
       GdiplusShutdown(m_gdiplusToken);
    }
    
  7. Implemente el método CChildView::DrawMandelbrot. Este método dibuja el fractal de Mandelbrot en el 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. Implemente el método CChildView::OnPaint. Este método llama a CChildView::DrawMandelbrot y, luego, copia el contenido del objeto Bitmap en la ventana.

    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. Compruebe que la aplicación se actualizó correctamente; para ello, compílela y ejecútela.

En la ilustración siguiente se muestran los resultados de la aplicación Mandelbrot.

The Mandelbrot Application.

Dado que el cálculo de cada píxel es costoso a nivel computacional, el subproceso de interfaz de usuario no puede procesar mensajes adicionales hasta que finalice el cálculo general. Esto podría disminuir la capacidad de respuesta de la aplicación. Aun así, puede aliviar este problema si quita el trabajo del subproceso de interfaz de usuario.

[Arriba]

Eliminación de trabajo del subproceso de interfaz de usuario

En esta sección se muestra cómo quitar el trabajo de dibujo del subproceso de interfaz de usuario en la aplicación Mandelbrot. Al mover el trabajo de dibujo del subproceso de interfaz de usuario a un subproceso de trabajo, el subproceso de interfaz de usuario puede procesar mensajes a medida que el subproceso de trabajo genera la imagen en segundo plano.

El Runtime de simultaneidad proporciona tres maneras de ejecutar tareas: grupos de tareas, agentes asincrónicos y tareas ligeras. Aunque puede usar cualquiera de estos mecanismos para quitar el trabajo del subproceso de interfaz de usuario, en este ejemplo se usa un objeto concurrency::task_group porque los grupos de tareas admiten la cancelación. En este tutorial se usa más adelante la cancelación para reducir la cantidad de trabajo que se realiza al cambiar el tamaño de la ventana del cliente y para realizar la limpieza al destruir la ventana.

En este ejemplo también se usa un objeto concurrency::unbounded_buffer para permitir que el subproceso de interfaz de usuario y el subproceso de trabajo se comuniquen entre sí. Una vez que el subproceso de trabajo genera la imagen, envía un puntero al objeto Bitmap al objeto unbounded_buffer y, luego, publica un mensaje de dibujo en el subproceso de interfaz de usuario. Después, el subproceso de interfaz de usuario recibe del unbounded_buffer objeto el objeto Bitmap y lo dibuja en la ventana del cliente.

Para quitar el trabajo de dibujo del subproceso de interfaz de usuario

  1. En pch.h (stdafx.h en Visual Studio 2017 y versiones anteriores), agregue las siguientes directivas #include:

    #include <agents.h>
    #include <ppl.h>
    
  2. En ChildView.h, agregue variables miembro task_group y unbounded_buffer a la sección protected de la clase CChildView. El objeto task_group contiene las tareas que realizan el dibujo; el objeto unbounded_buffer contiene la imagen de Mandelbrot completada.

    concurrency::task_group m_DrawingTasks;
    concurrency::unbounded_buffer<BitmapPtr> m_MandelbrotImages;
    
  3. En ChildView.cpp, agregue una directiva using al espacio de nombres concurrency.

    using namespace concurrency;
    
  4. En el método CChildView::DrawMandelbrot, después de la llamada a Bitmap::UnlockBits, llame a la función concurrency::send para pasar el objeto Bitmap al subproceso de interfaz de usuario. Después, publique un mensaje de dibujo en el subproceso de interfaz de usuario e invalide el área 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. Actualice el método CChildView::OnPaint para recibir el objeto Bitmap actualizado y dibujar la imagen en la ventana del 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)));
          });
       }
    }
    

    El método CChildView::OnPaint crea una tarea para generar la imagen de Mandelbrot si no existe en el búfer de mensajes. El búfer de mensajes no contendrá un objeto Bitmap en casos como el mensaje de dibujo inicial y cuando se mueve otra ventana delante de la ventana del cliente.

  6. Compruebe que la aplicación se actualizó correctamente; para ello, compílela y ejecútela.

La interfaz de usuario tiene ahora una mayor capacidad de respuesta porque el trabajo de dibujo se realiza en segundo plano.

[Arriba]

Mejora del rendimiento del dibujo

La generación del fractal de Mandelbrot es una buena candidata para la paralelización, ya que el cálculo de cada píxel es independiente de todos los demás cálculos. Para paralelizar el procedimiento de dibujo, convierta el bucle externo for en el método CChildView::DrawMandelbrot en una llamada a la algoritmo simultaneidad::p arallel_for, como se indica a continuación.

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

Dado que el cálculo de cada elemento de mapa de bits es independiente, no es necesario sincronizar las operaciones de dibujo que acceden a la memoria del mapa de bits. Esto permite escalar el rendimiento a medida que aumenta el número de procesadores disponibles.

[Arriba]

Adición de compatibilidad con la cancelación

En esta sección se describe cómo controlar el cambio de tamaño de la ventana y cómo cancelar las tareas de dibujo activas cuando se destruye la ventana.

En el documento Cancelación en la biblioteca PPL se explica cómo funciona la cancelación en tiempo de ejecución. La cancelación es cooperativa; por lo tanto, no se produce inmediatamente. Para detener una tarea cancelada, el tiempo de ejecución produce una excepción interna durante una llamada posterior desde la tarea en el tiempo de ejecución. En la sección anterior se muestra cómo usar el algoritmo parallel_for para mejorar el rendimiento de la tarea de dibujo. La llamada a parallel_for permite al tiempo de ejecución detener la tarea y, por lo tanto, permite que la cancelación funcione.

Cancelación de tareas activas

La aplicación Mandelbrot crea objetos Bitmap cuyas dimensiones coinciden con el tamaño de la ventana del cliente. Cada vez que se cambia el tamaño de la ventana del cliente, la aplicación crea una tarea en segundo plano adicional para generar una imagen para el nuevo tamaño de la ventana. La aplicación no requiere estas imágenes intermedias; solo necesita la imagen para el tamaño final de la ventana. Para evitar que la aplicación realice este trabajo adicional, puede cancelar cualquier tarea de dibujo activa en los controladores de mensajes de los mensajes WM_SIZE y WM_SIZING y, luego, volver a programar el trabajo de dibujo después de cambiar el tamaño de la ventana.

Para cancelar las tareas de dibujo activas cuando se cambia el tamaño de la ventana, la aplicación llama al método concurrency::task_group::cancel en los controladores de los mensajes WM_SIZING y WM_SIZE. El controlador del mensaje WM_SIZE también llama al método concurrency::task_group::wait para esperar a que se completen todas las tareas activas y, luego, vuelve a programar la tarea de dibujo para el tamaño actualizado de la ventana.

Cuando se destruye la ventana del cliente, se recomienda cancelar las tareas de dibujo activas. Si se cancelan las tareas de dibujo activas, se garantiza que los subprocesos de trabajo no publiquen mensajes en el subproceso de interfaz de usuario después de que se destruya la ventana del cliente. La aplicación cancela las tareas de dibujo activas en el controlador del mensaje WM_DESTROY.

Respuesta a la cancelación

El método CChildView::DrawMandelbrot, que realiza la tarea de dibujo, debe responder a la cancelación. Dado que el tiempo de ejecución usa el control de excepciones para cancelar las tareas, el método CChildView::DrawMandelbrot debe usar un mecanismo seguro para excepciones con el fin de garantizar que todos los recursos se limpien correctamente. En este ejemplo se usa el patrón Resource Acquisition Is Initialization (RAII) para garantizar que los bits del mapa de bits se desbloquean cuando se cancela la tarea.

Para agregar compatibilidad con la cancelación en la aplicación Mandelbrot
  1. En ChildView.h, en la sección protected de la clase CChildView, agregue declaraciones para las funciones de asignación de mensajes OnSize, OnSizing y 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. En ChildView.cpp, modifique el mapa de mensajes de modo que contenga controladores para los mensajes WM_SIZE, WM_SIZING y WM_DESTROY.

    BEGIN_MESSAGE_MAP(CChildView, CWnd)
       ON_WM_PAINT()
       ON_WM_SIZE()
       ON_WM_SIZING()
       ON_WM_DESTROY()
    END_MESSAGE_MAP()
    
  3. Implemente el método CChildView::OnSizing. Este método cancela las tareas de dibujo existentes.

    void CChildView::OnSizing(UINT nSide, LPRECT lpRect)
    {
       // The window size is changing; cancel any existing drawing tasks.
       m_DrawingTasks.cancel();
    }
    
  4. Implemente el método CChildView::OnSize. Este método cancela las tareas de dibujo existentes y crea una tarea de dibujo para el tamaño actualizado de la ventana del 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. Implemente el método CChildView::OnDestroy. Este método cancela las tareas de dibujo 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. En ChildView.cpp, defina la clase scope_guard, que implementa el patrón 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. Agregue el código siguiente al método CChildView::DrawMandelbrot después de la llamada a 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);      
    });
    

    Este código controla la cancelación mediante la creación de un objeto scope_guard. Cuando el objeto sale del ámbito, desbloquea los bits del mapa de bits.

  8. Modifique el final del método CChildView::DrawMandelbrot para descartar el objeto scope_guard después de desbloquear los bits del mapa de bits, pero antes de que se envíen mensajes al subproceso de interfaz de usuario. Esto garantiza que el subproceso de interfaz de usuario no se actualice antes de que se desbloqueen los bits del mapa de bits.

    // 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. Compruebe que la aplicación se actualizó correctamente; para ello, compílela y ejecútela.

Al cambiar el tamaño de la ventana, el trabajo de dibujo solo se realiza para el tamaño final de la ventana. Las tareas de dibujo activas también se cancelan cuando se destruye la ventana.

[Arriba]

Consulte también

Tutoriales del Runtime de simultaneidad
Paralelismo de tareas
Bloques de mensajes asincrónicos
Funciones que pasan mensajes
Algoritmos paralelos
Cancelación en la biblioteca PPL
Aplicaciones de escritorio de MFC