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++
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.
Tapez un nom pour le projet, par exemple,
Mandelbrot
puis cliquez sur OK pour afficher l’Assistant Application MFC.Dans le volet Type d’application, sélectionnez Document unique. Vérifiez que la case à cocher prise en charge de l’architecture document/affichage est désactivée.
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
Dans pch.h (stdafx.h dans Visual Studio 2017 et versions antérieures), ajoutez la directive suivante
#include
:#include <memory>
Dans ChildView.h, après la
pragma
directive, définissez leBitmapPtr
type. LeBitmapPtr
type permet à un pointeur vers unBitmap
objet d’être partagé par plusieurs composants. L’objetBitmap
est supprimé lorsqu’il n’est plus référencé par un composant.typedef std::shared_ptr<Gdiplus::Bitmap> BitmapPtr;
Dans ChildView.h, ajoutez le code suivant à la
protected
section de laCChildView
classe :protected: // Draws the Mandelbrot fractal to the specified Bitmap object. void DrawMandelbrot(BitmapPtr); protected: ULONG_PTR m_gdiplusToken;
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+.Dans ChildView.cpp, ajoutez une
using
directive à l’espaceGdiplus
de noms.using namespace Gdiplus;
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); }
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); }
Implémentez la méthode
CChildView::OnPaint
. Cette méthode appelleCChildView::DrawMandelbrot
, puis copie le contenu de l’objetBitmap
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); } }
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.
É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 le 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 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
Dans pch.h (stdafx.h dans Visual Studio 2017 et versions antérieures), ajoutez les directives suivantes
#include
:#include <agents.h> #include <ppl.h>
Dans ChildView.h, ajoutez
task_group
etunbounded_buffer
membres des variables à laprotected
section de laCChildView
classe. L’objettask_group
contient les tâches qui effectuent le dessin ; l’objetunbounded_buffer
contient l’image Mandelbrot terminée.concurrency::task_group m_DrawingTasks; concurrency::unbounded_buffer<BitmapPtr> m_MandelbrotImages;
Dans ChildView.cpp, ajoutez une
using
directive à l’espaceconcurrency
de noms.using namespace concurrency;
Dans la
CChildView::DrawMandelbrot
méthode, après l’appel àBitmap::UnlockBits
, appelez la fonction concurrency ::send pour transmettre l’objetBitmap
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);
Mettez à jour la
CChildView::OnPaint
méthode pour recevoir l’objet mis à jourBitmap
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’objetBitmap
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.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 nettoyées. 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
Dans ChildView.h, dans la
protected
section de laCChildView
classe, ajoutez des déclarations pour lesOnSize
fonctions ,OnSizing
etOnDestroy
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()
Dans ChildView.cpp, modifiez la carte de messages pour contenir des gestionnaires pour les messages
WM_SIZING
etWM_DESTROY
lesWM_SIZE
messages.BEGIN_MESSAGE_MAP(CChildView, CWnd) ON_WM_PAINT() ON_WM_SIZE() ON_WM_SIZING() ON_WM_DESTROY() END_MESSAGE_MAP()
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(); }
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))); }); } }
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(); }
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&); };
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.Modifiez la fin de la
CChildView::DrawMandelbrot
méthode pour ignorer l’objetscope_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);
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