Procédure pas à pas : suppression de travail d'un thread d'interface utilisateur

Ce document montre comment utiliser le runtime d’accès concurrentiel pour déplacer le travail effectué par le thread d’interface utilisateur (UI) d’une application Microsoft Foundation Classes (MFC) vers un thread de travail. Ce document montre également comment améliorer les performances d’une longue opération de dessin.

La suppression du travail du thread d’interface utilisateur en déchargeant les opérations bloquantes, par exemple, le dessin, vers les threads de travail peut améliorer la réactivité de votre application. Cette procédure pas à pas utilise une routine de dessin qui génère le fractal DeBlobrot pour illustrer une longue opération bloquante. La génération de la fractal de Udpbrot est également un bon candidat à la parallélisation, car le calcul de chaque pixel est indépendant de tous les autres calculs.

Prérequis

Lisez les rubriques suivantes avant de commencer cette procédure pas à pas :

Nous vous recommandons également de comprendre les principes de base du développement d’applications MFC et GDI+ avant de commencer cette procédure pas à pas. Pour plus d’informations sur MFC, consultez applications de bureau MFC. Pour plus d’informations sur GDI+, consultez GDI+.

Sections

Cette procédure pas à pas contient les sections suivantes :

Création de l’application MFC

Cette section explique comment créer l’application MFC de base.

Pour créer une application MFC Visual C++

  1. Utilisez l’Assistant Application MFC pour créer une application MFC avec tous les paramètres par défaut. Consultez la procédure pas à pas : utilisation des nouveaux contrôles Shell MFC pour obtenir des instructions sur l’ouverture de l’Assistant pour votre version de Visual Studio.

  2. Tapez un nom pour le projet, par exemple, Mandelbrotpuis cliquez sur OK pour afficher l’Assistant Application MFC.

  3. Dans le volet Type d’application, sélectionnez Document unique. Vérifiez que l’architecture document/affichage prend en charge case activée zone est désactivée.

  4. Cliquez sur Terminer pour créer le projet et fermer l’Assistant Application MFC.

    Vérifiez que l’application a été créée correctement en le créant et en l’exécutant. Pour générer l’application, dans le menu Générer , cliquez sur Générer la solution. Si l’application est générée avec succès, exécutez l’application en cliquant sur Démarrer le débogage dans le menu Débogage .

Implémentation de la version série de l’application Mandelbrot

Cette section explique comment dessiner le fractal de Mandelbrot. Cette version dessine le fractal De Mandelbrot sur un objet Bitmap GDI+, puis copie le contenu de cette bitmap dans la fenêtre cliente.

Pour implémenter la version série de l’application Mandelbrot

  1. Dans pch.h (stdafx.h dans Visual Studio 2017 et versions antérieures), ajoutez la directive suivante #include :

    #include <memory>
    
  2. Dans ChildView.h, après la pragma directive, définissez le BitmapPtr type. Le BitmapPtr type permet à un pointeur vers un Bitmap objet d’être partagé par plusieurs composants. L’objet Bitmap est supprimé lorsqu’il n’est plus référencé par un composant.

    typedef std::shared_ptr<Gdiplus::Bitmap> BitmapPtr;
    
  3. Dans ChildView.h, ajoutez le code suivant à la protected section de la CChildView classe :

    protected:
       // Draws the Mandelbrot fractal to the specified Bitmap object.
       void DrawMandelbrot(BitmapPtr);
    
    protected:
       ULONG_PTR m_gdiplusToken;
    
  4. Dans ChildView.cpp, commentez ou supprimez les lignes suivantes.

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

    Dans les builds Debug, cette étape empêche l’application d’utiliser l’allocateur DEBUG_NEW , qui est incompatible avec GDI+.

  5. Dans ChildView.cpp, ajoutez une using directive à l’espace Gdiplus de noms.

    using namespace Gdiplus;
    
  6. Ajoutez le code suivant au constructeur et au destructeur de la CChildView classe pour initialiser et arrêter GDI+.

    CChildView::CChildView()
    {
       // Initialize GDI+.
       GdiplusStartupInput gdiplusStartupInput;
       GdiplusStartup(&m_gdiplusToken, &gdiplusStartupInput, NULL);
    }
    
    CChildView::~CChildView()
    {
       // Shutdown GDI+.
       GdiplusShutdown(m_gdiplusToken);
    }
    
  7. Implémentez la méthode CChildView::DrawMandelbrot. Cette méthode dessine le fractal de Mandelbrot sur l’objet spécifié 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. Implémentez la méthode CChildView::OnPaint. Cette méthode appelle CChildView::DrawMandelbrot , puis copie le contenu de l’objet Bitmap dans la fenêtre.

    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. Vérifiez que l’application a été mise à jour correctement en le générant et en l’exécutant.

L’illustration suivante montre les résultats de l’application Mandelbrot.

The Mandelbrot Application.

Étant donné que le calcul de chaque pixel est coûteux en calcul, le thread d’interface utilisateur ne peut pas traiter des messages supplémentaires tant que le calcul global n’est pas terminé. Cela peut réduire la réactivité dans l’application. Toutefois, vous pouvez soulager ce problème en supprimant le travail du thread d’interface utilisateur.

[Haut]

Suppression du travail du thread d’interface utilisateur

Cette section montre comment supprimer le travail de dessin du thread d’interface utilisateur dans l’application Mandelbrot. En déplaçant le travail de dessin du thread d’interface utilisateur vers un thread de travail, le thread d’interface utilisateur peut traiter les messages lorsque le thread de travail génère l’image en arrière-plan.

Le runtime d’accès concurrentiel fournit trois façons d’exécuter des tâches : des groupes de tâches, des agents asynchrones et des tâches légères. Bien que vous puissiez utiliser l’un de ces mécanismes pour supprimer le travail du thread d’interface utilisateur, cet exemple utilise un objet concurrency ::task_group , car les groupes de tâches prennent en charge l’annulation. Cette procédure pas à pas utilise ultérieurement l’annulation pour réduire la quantité de travail effectuée lorsque la fenêtre cliente est redimensionnée et pour effectuer propre up lorsque la fenêtre est détruite.

Cet exemple utilise également un objet concurrency ::unbounded_buffer pour permettre au thread d’interface utilisateur et au thread de travail de communiquer entre eux. Une fois que le thread de travail produit l’image, il envoie un pointeur à l’objet Bitmap à l’objet unbounded_buffer , puis publie un message de peinture sur le thread d’interface utilisateur. Le thread d’interface utilisateur reçoit ensuite de l’objet unbounded_buffer l’objet Bitmap et le dessine dans la fenêtre cliente.

Pour supprimer le travail de dessin du thread d’interface utilisateur

  1. Dans pch.h (stdafx.h dans Visual Studio 2017 et versions antérieures), ajoutez les directives suivantes #include :

    #include <agents.h>
    #include <ppl.h>
    
  2. Dans ChildView.h, ajoutez task_group et unbounded_buffer membres des variables à la protected section de la CChildView classe. L’objet task_group contient les tâches qui effectuent le dessin ; l’objet unbounded_buffer contient l’image Mandelbrot terminée.

    concurrency::task_group m_DrawingTasks;
    concurrency::unbounded_buffer<BitmapPtr> m_MandelbrotImages;
    
  3. Dans ChildView.cpp, ajoutez une using directive à l’espace concurrency de noms.

    using namespace concurrency;
    
  4. Dans la CChildView::DrawMandelbrot méthode, après l’appel à Bitmap::UnlockBits, appelez la fonction concurrency ::send pour transmettre l’objet Bitmap au thread d’interface utilisateur. Publiez ensuite un message de peinture sur le thread d’interface utilisateur et invalidez la zone 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. Mettez à jour la CChildView::OnPaint méthode pour recevoir l’objet mis à jour Bitmap et dessiner l’image dans la fenêtre 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)));
          });
       }
    }
    

    La CChildView::OnPaint méthode crée une tâche pour générer l’image Mandelbrot si elle n’existe pas dans la mémoire tampon du message. La mémoire tampon de message ne contient pas d’objet Bitmap dans les cas tels que le message de peinture initial et quand une autre fenêtre est déplacée devant la fenêtre cliente.

  6. Vérifiez que l’application a été mise à jour correctement en le générant et en l’exécutant.

L’interface utilisateur est désormais plus réactive, car le travail de dessin est effectué en arrière-plan.

[Haut]

Amélioration des performances de dessin

La génération de la fractal de Udpbrot est un bon candidat à la parallélisation, car le calcul de chaque pixel est indépendant de tous les autres calculs. Pour paralléliser la procédure de dessin, convertissez la boucle externe for dans la CChildView::DrawMandelbrot méthode en un appel à l’algorithme concurrency ::p arallel_for , comme suit.

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

Étant donné que le calcul de chaque élément bitmap est indépendant, vous n’avez pas besoin de synchroniser les opérations de dessin qui accèdent à la mémoire bitmap. Cela permet de mettre à l’échelle les performances à mesure que le nombre de processeurs disponibles augmente.

[Haut]

Ajout de la prise en charge de l’annulation

Cette section explique comment gérer le redimensionnement de fenêtre et comment annuler les tâches de dessin actives lorsque la fenêtre est détruite.

Le document Cancellation in the PPL explique comment l’annulation fonctionne dans le runtime. L’annulation est coopérative ; par conséquent, elle ne se produit pas immédiatement. Pour arrêter une tâche annulée, le runtime lève une exception interne lors d’un appel ultérieur de la tâche dans le runtime. La section précédente montre comment utiliser l’algorithme parallel_for pour améliorer les performances de la tâche de dessin. L’appel permettant au parallel_for runtime d’arrêter la tâche et permet donc l’annulation de fonctionner.

Annulation des tâches actives

L’application Mandelbrot crée Bitmap des objets dont les dimensions correspondent à la taille de la fenêtre cliente. Chaque fois que la fenêtre cliente est redimensionnée, l’application crée une tâche d’arrière-plan supplémentaire pour générer une image pour la nouvelle taille de fenêtre. L’application ne nécessite pas ces images intermédiaires ; elle ne nécessite que l’image pour la taille finale de la fenêtre. Pour empêcher l’application d’effectuer ce travail supplémentaire, vous pouvez annuler toutes les tâches de dessin actives dans les gestionnaires de messages pour les WM_SIZE messages et WM_SIZING les messages, puis replanifier le travail de dessin une fois la fenêtre redimensionnée.

Pour annuler les tâches de dessin actives lorsque la fenêtre est redimensionnée, l’application appelle la méthode concurrency ::task_group ::cancel dans les gestionnaires pour les messages et WM_SIZE les WM_SIZING messages. Le gestionnaire du WM_SIZE message appelle également la méthode concurrency ::task_group ::wait pour attendre que toutes les tâches actives se terminent, puis replanifient la tâche de dessin pour la taille de fenêtre mise à jour.

Lorsque la fenêtre cliente est détruite, il est recommandé d’annuler les tâches de dessin actives. L’annulation de toutes les tâches de dessin actives garantit que les threads de travail ne publient pas de messages dans le thread d’interface utilisateur une fois la fenêtre cliente détruite. L’application annule toutes les tâches de dessin actives dans le gestionnaire du WM_DESTROY message.

Réponse à l’annulation

La CChildView::DrawMandelbrot méthode, qui effectue la tâche de dessin, doit répondre à l’annulation. Étant donné que le runtime utilise la gestion des exceptions pour annuler les tâches, la CChildView::DrawMandelbrot méthode doit utiliser un mécanisme sans risque d’exception pour garantir que toutes les ressources sont correctement propre ed-up. Cet exemple utilise le modèle d’initialisation d’acquisition de ressources (RAII) pour garantir que les bits bitmap sont déverrouillés lorsque la tâche est annulée.

Pour ajouter la prise en charge de l’annulation dans l’application Mandelbrot
  1. Dans ChildView.h, dans la protected section de la CChildView classe, ajoutez des déclarations pour les OnSizefonctions , OnSizinget OnDestroy de mappage de messages.

    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. Dans ChildView.cpp, modifiez le mappage de messages pour contenir des gestionnaires pour les messages et WM_DESTROY les WM_SIZEWM_SIZINGmessages.

    BEGIN_MESSAGE_MAP(CChildView, CWnd)
       ON_WM_PAINT()
       ON_WM_SIZE()
       ON_WM_SIZING()
       ON_WM_DESTROY()
    END_MESSAGE_MAP()
    
  3. Implémentez la méthode CChildView::OnSizing. Cette méthode annule les tâches de dessin existantes.

    void CChildView::OnSizing(UINT nSide, LPRECT lpRect)
    {
       // The window size is changing; cancel any existing drawing tasks.
       m_DrawingTasks.cancel();
    }
    
  4. Implémentez la méthode CChildView::OnSize. Cette méthode annule toutes les tâches de dessin existantes et crée une tâche de dessin pour la taille de fenêtre cliente mise à jour.

    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. Implémentez la méthode CChildView::OnDestroy. Cette méthode annule les tâches de dessin existantes.

    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. Dans ChildView.cpp, définissez la scope_guard classe, qui implémente le modèle 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. Ajoutez le code suivant à la CChildView::DrawMandelbrot méthode après l’appel à 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);      
    });
    

    Ce code gère l’annulation en créant un scope_guard objet. Lorsque l’objet quitte l’étendue, il déverrouille les bits bitmap.

  8. Modifiez la fin de la CChildView::DrawMandelbrot méthode pour ignorer l’objet scope_guard une fois les bits bitmap déverrouillés, mais avant que les messages ne soient envoyés au thread d’interface utilisateur. Cela garantit que le thread d’interface utilisateur n’est pas mis à jour avant que les bits bitmap ne soient déverrouillés.

    // 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. Vérifiez que l’application a été mise à jour correctement en le générant et en l’exécutant.

Lorsque vous redimensionnez la fenêtre, le travail de dessin est effectué uniquement pour la taille finale de la fenêtre. Toutes les tâches de dessin actives sont également annulées lorsque la fenêtre est détruite.

[Haut]

Voir aussi

Procédures pas à pas relatives au runtime d’accès concurrentiel
Parallélisme des tâches
Blocs de messages asynchrones
Fonctions de passage de messages
Algorithmes parallèles
Annulation dans la bibliothèque de modèles parallèles
MFC, applications de bureau