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

En este documento se muestra cómo se usa el Runtime de simultaneidad para trasladar el trabajo realizado por un subproceso de la interfaz de usuario de una aplicación Microsoft Foundation Classes (MFC) a un subproceso de trabajo. En este documento se muestra también cómo puede mejorarse el rendimiento de una operación de dibujo larga.

La capacidad de respuesta de una aplicación puede mejorar si se quita trabajo de un subproceso de la interfaz de usuario trasladando las operaciones en bloque, como por ejemplo las operaciones de dibujo, a subprocesos de trabajo. En este tutorial se usa una rutina de dibujo que genera el fractal de Mandelbrot para ilustrar una operación en bloque larga. La generación del fractal de Mandelbrot también resulta adecuada para la paralelización, ya que el cálculo de cada píxel se realiza independientemente de todos los demás cálculos.

Requisitos previos

Lea los siguientes temas antes de iniciar este tutorial:

También es recomendable que conozca los fundamentos de desarrollo de la aplicación MFC y GDI+ antes de iniciar este tutorial. Para obtener más información sobre MFC, vea MFC Reference. Para obtener más información sobre GDI+, vea GDI+.

Secciones

Este tutorial contiene las siguientes secciones:

  • Crear la aplicación MFC

  • Implementar la versión de serie de la aplicación Mandelbrot

  • Quitar trabajo del subproceso de la interfaz de usuario

  • Mejorar el rendimiento de las operaciones de dibujo

  • Agregar compatibilidad con la operación de cancelación

Crear la aplicación MFC

En esta sección se describe cómo se crea una aplicación MFC básica.

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

  1. En el menú Archivo, haga clic en Nuevo y, a continuación, haga clic en Proyecto.

  2. En el cuadro de diálogo Nuevo proyecto, en el panel Plantillas instaladas, seleccione Visual C++ y, a continuación, seleccione Aplicación MFC en el recuadro Plantillas. Escriba un nombre para el proyecto, por ejemplo, Mandelbrot y, a continuación, haga clic en Aceptar para mostrar el Asistente para aplicaciones MFC.

  3. En el recuadro Tipo de aplicación, seleccione Documento único. Asegúrese de que la casilla 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ú Generar, haga clic en Generar solución. Si la aplicación se compila correctamente, en el menú Depurar, haga clic en Iniciar depuración para ejecutarla.

Implementar la versión de serie de la aplicación Mandelbrot

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

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

  1. En stdafx.h, agregue la directiva #include siguiente:

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

    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, marque como comentario o quite las siguientes líneas.

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

    En las compilaciones de depuración, este paso impide que la aplicación use el asignador de 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 siguiente código al constructor y destructor de la clase CChildView para inicializar y cerrar 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 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 ha actualizado correctamente; para ello, compílela y ejecútela.

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

La aplicación Mandelbrot

Como el cálculo de cada píxel consume muchos recursos, el subproceso de la interfaz de usuario no puede procesar otros mensajes hasta que finaliza todo el cálculo. Esto puede reducir la capacidad de respuesta de la aplicación. Sin embargo, puede aliviar este problema quitando el trabajo del subproceso de la interfaz de usuario.

[Ir al principio]

Quitar el trabajo del subproceso de la interfaz de usuario

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

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

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

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

  1. En stdafx.h, agregue las directivas #include siguientes:

    #include <agents.h>
    #include <ppl.h>
    
  2. En ChildView.h, agregue las 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 las operaciones de dibujo; el objeto unbounded_buffer contiene la imagen completada de Mandelbrot.

    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 la interfaz de usuario. A continuación, envíe un mensaje del dibujo al subproceso de la 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 que va a recibir el objeto Bitmap actualizado y dibuje 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 ninguna en el búfer de mensajes. El búfer de mensajes no contendrá ningún objeto Bitmap en casos como el del mensaje del dibujo inicial y cuando otra ventana se sitúa delante de la ventana de cliente.

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

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

[Ir al principio]

Mejorar el rendimiento de las operaciones de dibujo

La generación del fractal de Mandelbrot también resulta adecuada para la paralelización, ya que el cálculo de cada píxel se realiza independientemente 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 al algoritmo Concurrency::parallel_for, tal y como se muestra 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 tiene que sincronizar las operaciones de dibujo que tienen acceso a la memoria del mapa de bits. De este modo, se puede escalar el rendimiento cuando aumenta el número de procesadores disponibles.

[Ir al principio]

Agregar compatibilidad con la operación de cancelación

En esta sección se describe cómo se administra el cambio de tamaño de la ventana y cómo se cancelan 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 el runtime. La cancelación es una operación cooperativa; por tanto, no se produce inmediatamente. Para detener una tarea cancelada, el runtime inicia una excepción interna durante una llamada posterior desde la tarea al runtime. En la sección anterior se muestra cómo se usa el algoritmo parallel_for para mejorar el rendimiento de la tarea de dibujo. La llamada a parallel_for permite al runtime detener la tarea y, por tanto, permite que la cancelación funcione.

Cancelar tareas activas

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

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

Cuando se destruye la ventana del cliente, resulta conveniente cancelar las tareas de dibujo activas. Al cancelar las tareas de dibujo activas, se asegura de que los subprocesos de trabajo no envían mensajes al subproceso de interfaz de usuario una vez que la ventana del cliente se ha destruido. La aplicación cancela las tareas de dibujo activas en el controlador del mensaje WM_DESTROY.

Responder a la cancelación

El método CChildView::DrawMandelbrot, que realiza la tarea de dibujo, debe responder a la cancelación. Como el runtime usa el control de excepciones para cancelar tareas, el método CChildView::DrawMandelbrot debe usar un mecanismo seguro ante excepciones para garantizar que todos los recursos se limpian correctamente. En este ejemplo se usa el modelo 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 operación de cancelación en la aplicación Mandelbrot

  1. En ChildView.h, en la sección protected de la clase CChildView, agregue las declaraciones de las funciones de asignación de mensajes OnSize, OnDestroy y OnSizing.

    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 la asignación de mensajes para que incluya controladores de 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 nueva 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 modelo 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 administra la cancelación creando un objeto scope_guard. Cuando el objeto abandona el ámbito, desbloquea los bits del mapa de bits.

  8. Modifique el final del método CChildView::DrawMandelbrot para desechar el objeto scope_guard una vez que se desbloquean los bits del mapa de bits, pero antes de que se envíe ningún mensaje al subproceso de interfaz de usuario. De este modo, se asegura de que el subproceso de interfaz de usuario no se actualiza antes de que los bits del mapa de bits se desbloqueen.

    // 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 ha actualizado correctamente; para ello, compílela y ejecútela.

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

[Ir al principio]

Vea también

Conceptos

Tutoriales del Runtime de simultaneidad

Paralelismo de tareas (Runtime de simultaneidad)

Bloques de mensajes asincrónicos

Funciones que pasan mensajes

Algoritmos paralelos

Cancelación en la biblioteca PPL

Otros recursos

MFC Reference