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 exécuté par le thread d'interface utilisateur dans 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.

En déplaçant un travail du thread d'interface utilisateur par le déchargement des opérations bloquantes, telles que le dessin, vers des 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 une fractale de Mandelbrot pour démontrer une longue opération bloquante. La génération de fractales de Mandelbrot est aussi une candidate idéale pour la parallélisation car le calcul de chaque pixel est indépendant de tous les autres calculs.

Composants requis

Lisez les rubriques suivantes avant de démarrer cette procédure pas-à-pas :

Nous vous recommandons également de connaître les bases du développement d'applications MFC et de GDI+ avant de démarrer cette procédure pas à pas. Pour plus d'informations concernant MFC, consultez MFC Reference. Pour plus d'informations sur GDI+, consultez GDI+.

Sections

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

  • Création d'une application MFC

  • Implémentation de la version sérialisée de l'application Mandelbrot

  • Suppression d'un travail du thread d'interface utilisateur

  • Amélioration des performances de dessin

  • Ajout de prise en charge pour l'annulation

Création d'une application MFC

Cette section décrit comment créer une application MFC de base.

Pour créer une application MFC en Visual C++

  1. Dans le menu Fichier, cliquez sur Nouveau, puis sur Projet.

  2. Dans la boîte de dialogue Nouveau projet, sélectionnez Visual C++ dans le volet , puis cliquez sur Application MFC dans le volet Modèles. Tapez un nom pour le projet, par exemple Mandelbrot, puis cliquez sur OK pour afficher l'Assistant Application MFC.

  3. Dans le volet Type d'application, sélectionnez Document unique. Assurez-vous que la case à cocher Prise en charge de l'architecture Document/Vue 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 avec succès en la générant 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-la en cliquant sur le bouton Démarrer le débogage dans le menu Déboguer.

Implémentation de la version sérialisée de l'application Mandelbrot

Cette section décrit comment dessiner une fractale de Mandelbrot. Cette version dessine la fractale de Mandelbrot dans un objet GDI+Bitmap puis copie le contenu de cet objet bitmap dans la fenêtre cliente.

Pour implémenter la version sérialisée de l'application Mandelbrot

  1. Dans stdafx.h, ajoutez la directive #include suivante :

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

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

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

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

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

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

    using namespace Gdiplus;
    
  6. Ajoutez le code suivant au constructeur et destructeur de la classe CChildView 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 la fractale de Mandelbrot dans l'objet Bitmap spécifié.

    // 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 vers 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 avec succès en la générant et en l'exécutant.

L'illustration suivante présente les résultats de l'application Mandelbrot.

L'application Mandelbrot

Étant donné que le calcul de chaque pixel est gourmand en ressources informatiques, le thread d'interface utilisateur ne peut pas traiter de messages supplémentaires avant la fin du calcul final. Cela peut diminuer la réactivité dans l'application. Toutefois, il est possible d'atténuer ce problème en supprimant du travail d'un thread d'interface utilisateur.

[retour en haut]

Suppression de travail d'un thread d'interface utilisateur

Cette section indique comment supprimer du travail de dessin d'un thread d'interface utilisateur dans l'application Mandelbrot. En déplaçant du travail de dessin d'un thread d'interface utilisateur vers un thread de travail, vous permettez au thread d'interface utilisateur de traiter des messages pendant que le thread de travail génère l'image en arrière-plan.

Le runtime d'accès concurrentiel offre trois moyens d'exécuter des tâches : groupes de tâches, agents asynchrones et tâches légères. Bien que vous puissiez utiliser n'importe lequel de ces mécanismes pour supprimer du travail d'un 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 plus tard l'annulation pour réduire la quantité de travail exécutée lorsque la fenêtre cliente est redimensionnée et pour effectuer un nettoyage 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 l'un avec l'autre. Après avoir produit l'image, le thread de travail envoie un pointeur de l'objet Bitmap vers l'objet unbounded_buffer puis envoie un message de peinture au 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 du travail de dessin d'un thread d'interface utilisateur

  1. Dans stdafx.h, ajoutez les directives #include suivantes :

    #include <agents.h>
    #include <ppl.h>
    
  2. Dans ChildView.h, ajoutez des variables membres task_group et unbounded_buffer à la section protected de la classe CChildView. L'objet task_group contient les tâches qui exécutent 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 directive using à l'espace de noms Concurrency.

    using namespace Concurrency;
    
  4. Dans la méthode CChildView::DrawMandelbrot, après l'appel à Bitmap::UnlockBits, appelez la fonction Concurrency::send pour passer l'objet Bitmap au thread d'interface utilisateur. Envoyez ensuite un message de peinture au thread d'interface utilisateur et invalidez la zone client.

    // 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 méthode CChildView::OnPaint pour recevoir l'objet Bitmap mis à jour 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 méthode CChildView::OnPaint crée une tâche pour générer l'image Mandelbrot si une image n'est pas déjà présente dans le tampon de messages. Le tampon de messages ne contiendra pas d'objet Bitmap pour le message de peinture initial et lorsqu'une autre fenêtre est déplacée devant la fenêtre cliente.

  6. Vérifiez que l'application a été mise à jour avec succès en la générant et en l'exécutant.

L'interface utilisateur est maintenant plus réactive car le travail de dessin est exécuté en arrière-plan.

[retour en haut]

Amélioration des performances de dessin

La génération de fractales de Mandelbrot est une candidate idéale pour 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 for externe dans la méthode CChildView::DrawMandelbrot en un appel à l'algorithme Concurrency::parallel_for, de la manière suivante.

// 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 de bitmap est indépendant, vous n'avez pas à synchroniser les opérations de dessin qui accèdent à la mémoire bitmap. Cela permet aux performances de s'adapter à l'augmentation des processeurs disponibles.

[retour en haut]

Ajout de prise en charge pour l'annulation

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

Le document Annulation dans la bibliothèque de modèles parallèles explique le fonctionnement de l'annulation 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 pendant un appel suivant, depuis la tâche dans le runtime. La section précédente indique comment utiliser l'algorithme parallel_for pour améliorer les performances de tâche de dessin. L'appel à parallel_for permet à l'exécution d'arrêter la tâche, et par conséquent, à l'annulation de fonctionner.

Annulation des tâches actives

L'application Mandelbrot crée des objets Bitmap 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 supplémentaire en arrière-plan pour générer une image pour la nouvelle taille de la fenêtre. L'application ne requiert pas ces images intermédiaires mais uniquement l'image pour la dernière taille de la fenêtre. Pour empêcher l'application d'exécuter ce travail supplémentaire, vous pouvez annuler toutes les tâches de dessin actives dans les gestionnaires de messages pour les messages WM_SIZING et WM_SIZE , puis replanifier le travail de dessin une fois la fenêtre redimensionnée.

Pour annuler des 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 WM_SIZE et WM_SIZING. Le gestionnaire du message WM_SIZE appelle également la méthode Concurrency::task_group::wait pour attendre que toutes les tâches actives soient terminées, puis replanifie la tâche de dessin pour la taille de la fenêtre mise à jour.

Lorsque la fenêtre cliente est détruite, il s'agit de la méthode conseillée pour annuler toutes les tâches de dessin actives. L'annulation de toutes les tâches de dessin actives permet de s'assurer que les threads de travail n'envoient pas de messages au 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 pour le message WM_DESTROY.

Réponse à l'annulation

La méthode CChildView::DrawMandelbrot, qui exécute la tâche de dessin, doit répondre à l'annulation. Étant donné que l'exécution utilise la gestion des exceptions pour annuler des tâches, la méthode CChildView::DrawMandelbrot doit utiliser un mécanisme sécurisé du point de vue des exceptions pour garantir que toutes les ressources sont correctement nettoyées. Cet exemple utilise le modèle RAII (Resource Acquisition Is Initialization) pour garantir que les bits de la bitmap soient déverrouillés lorsque la tâche est annulée.

Pour ajouter une prise en charge de l'annulation à l'application Mandelbrot

  1. Dans ChildView.h, dans la section protected de la classe CChildView, ajoutez des déclarations pour les fonctions de table de messages OnSize, OnSizing et 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. Dans ChildView.cpp, modifiez la table des messages pour qu'elle contienne des gestionnaires pour les messages WM_SIZE, WM_SIZING et WM_DESTROY.

    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 toutes 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 la 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 toutes 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 classe scope_guard, 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 méthode CChildView::DrawMandelbrot 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 objet scope_guard. Lorsque l'objet quitte la portée, il déverrouille les bits de la bitmap.

  8. Modifiez la fin de la méthode CChildView::DrawMandelbrot pour renvoyer l'objet scope_guard une fois les bits de la bitmap déverrouillés, mais avant que tous les messages soient envoyés au thread d'interface utilisateur. Cela permet de s'assurer que le thread d'interface utilisateur n'est pas mis à jour avant que les bits de la 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 avec succès en la générant et en l'exécutant.

Lorsque vous redimensionnez la fenêtre, le travail de dessin est exécuté 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.

[retour en haut]

Voir aussi

Concepts

Procédures pas à pas relatives au runtime d'accès concurrentiel

Parallélisme des tâches (runtime d'accès concurrentiel)

Blocs de messages asynchrones

Fonctions de passage de messages

Algorithmes parallèles

Annulation dans la bibliothèque de modèles parallèles

Autres ressources

MFC Reference