Delen via


Stapsgewijze handleiding: Werk verwijderen uit een User-Interface-thread

In dit document wordt gedemonstreerd hoe u de Concurrency Runtime gebruikt om het werk dat wordt uitgevoerd door de gebruikersinterfacethread (UI) in een MFC-toepassing (Microsoft Foundation Classes) te verplaatsen naar een werkdraad. Dit document laat ook zien hoe u de prestaties van een langdurige tekenbewerking kunt verbeteren.

Het verwijderen van werk uit de UI-thread door blokkerende bewerkingen te offloaden, bijvoorbeeld tekenen, naar werkthreads kan de reactiesnelheid van uw toepassing verbeteren. Deze handleiding gebruikt een tekenroutine die de Mandelbrot-fractal genereert om een langdurige blokkeringsoperatie te demonstreren. De generatie van het Mandelbrot fractal is ook een goede kandidaat voor parallellisatie omdat de berekening van elke pixel onafhankelijk is van alle andere berekeningen.

Vereiste voorwaarden

Lees de volgende onderwerpen voordat u aan deze handleiding begint.

We raden u ook aan de basisprincipes van de ontwikkeling van MFC-toepassingen en GDI+ te begrijpen voordat u aan deze procedure begint. Zie MFC Desktop Applications voor meer informatie over MFC. Zie GDI+voor meer informatie over GDI+.

Afdelingen

Dit stappenplan bevat de volgende onderdelen:

Het maken van de MFC-toepassing

In deze sectie wordt beschreven hoe u de basis-MFC-toepassing maakt.

Een Visual C++ MFC-toepassing maken

  1. Gebruik de wizard MFC-toepassing om een MFC-toepassing te maken met alle standaardinstellingen. Zie Walkthrough: Met de nieuwe MFC Shell-besturingselementen voor instructies over het openen van de wizard voor uw versie van Visual Studio.

  2. Typ bijvoorbeeld een naam voor het project Mandelbroten klik vervolgens op OK om de wizard MFC-toepassing weer te geven.

  3. Selecteer één document in het deelvenster Toepassingstype. Zorg ervoor dat het selectievakje voor de ondersteuning van de document-/weergavearchitectuur is uitgeschakeld.

  4. Klik op Voltooien om het project te maken en sluit de wizard MFC-toepassing.

    Verifieer of de toepassing succesvol is aangemaakt door deze te bouwen en uit te voeren. Als u de toepassing wilt bouwen, klikt u in het menu Bouwen op Oplossing bouwen. Als de toepassing succesvol is gebouwd, voert u de toepassing uit door te klikken op Foutopsporing starten via het menu Foutopsporing.

De seriële versie van de Mandelbrot-toepassing implementeren

In deze sectie wordt beschreven hoe u de Mandelbrot fractal tekent. Deze versie tekent het Mandelbrot fractal naar een GDI+ Bitmap-object en kopieert vervolgens de inhoud van die bitmap naar het clientvenster.

De seriële versie van de Mandelbrot-toepassing implementeren

  1. Voeg in pch.h (stdafx.h in Visual Studio 2017 en eerder) de volgende #include instructie toe:

    #include <memory>
    
  2. In ChildView.h, na de pragma instructie, definieert u het BitmapPtr type. Met BitmapPtr het type kan een aanwijzer naar een Bitmap object worden gedeeld door meerdere onderdelen. Het Bitmap object wordt verwijderd wanneer er niet meer naar wordt verwezen door een onderdeel.

    typedef std::shared_ptr<Gdiplus::Bitmap> BitmapPtr;
    
  3. Voeg in ChildView.h de volgende code toe aan de protected sectie van de CChildView klasse:

    protected:
       // Draws the Mandelbrot fractal to the specified Bitmap object.
       void DrawMandelbrot(BitmapPtr);
    
    protected:
       ULONG_PTR m_gdiplusToken;
    
  4. Maak in ChildView.cpp opmerkingen of verwijder de volgende regels.

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

    In builds voor foutopsporing voorkomt u met deze stap dat de toepassing de DEBUG_NEW allocator gebruikt, wat niet compatibel is met GDI+.

  5. Voeg in ChildView.cpp een using instructie toe aan de Gdiplus naamruimte.

    using namespace Gdiplus;
    
  6. Voeg de volgende code toe aan de constructor en destructor van de CChildView klasse om GDI+ te initialiseren en af te sluiten.

    CChildView::CChildView()
    {
       // Initialize GDI+.
       GdiplusStartupInput gdiplusStartupInput;
       GdiplusStartup(&m_gdiplusToken, &gdiplusStartupInput, NULL);
    }
    
    CChildView::~CChildView()
    {
       // Shutdown GDI+.
       GdiplusShutdown(m_gdiplusToken);
    }
    
  7. Implementeer de methode CChildView::DrawMandelbrot. Met deze methode wordt het Mandelbrot fractal naar het opgegeven Bitmap object opgehaald.

    // 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. Implementeer de methode CChildView::OnPaint. Met deze methode wordt CChildView::DrawMandelbrot aangeroepen en vervolgens wordt de inhoud van het Bitmap object naar het venster gekopieerd.

    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. Controleer of de toepassing is bijgewerkt door deze te bouwen en uit te voeren.

In de volgende afbeelding ziet u de resultaten van de Mandelbrot-toepassing.

De Mandelbrot-toepassing.

Omdat de berekening voor elke pixel rekenkrachtig is, kan de UI-thread geen extra berichten verwerken totdat de algehele berekening is voltooid. Dit kan de reactiesnelheid in de toepassing verminderen. U kunt dit probleem echter oplossen door werk uit de UI-thread te verwijderen.

[Boven]

Werk verwijderen uit de UI-thread

In deze sectie ziet u hoe u het tekenwerk verwijdert uit de UI-thread in de Mandelbrot-toepassing. Door tekenwerk van de UI-thread naar een worker thread te verplaatsen, kan de UI-thread meldingen verwerken terwijl de worker thread de afbeelding op de achtergrond genereert.

De Gelijktijdigheidsruntime biedt drie manieren om taken uit te voeren: taakgroepen, asynchrone agents en lichtgewicht taken. Hoewel u een van deze mechanismen kunt gebruiken om werk uit de UI-thread te verwijderen, gebruikt dit voorbeeld een gelijktijdigheid::task_group-object omdat taakgroepen annulering ondersteunen. In dit scenario wordt later gebruikgemaakt van annulering om de hoeveelheid werk te verminderen die wordt uitgevoerd wanneer het clientvenster wordt gewijzigd en om opschoning uit te voeren wanneer het venster wordt vernietigd.

In dit voorbeeld wordt ook een concurrency::unbounded_buffer-object gebruikt om de UI-thread en de werkthread met elkaar te laten communiceren. Nadat de worker thread de afbeelding heeft geproduceerd, stuurt het een aanwijzer naar het Bitmap object naar het unbounded_buffer object en plaatst vervolgens een paintbericht naar de UI-thread. De UI-thread ontvangt vervolgens het unbounded_buffer object van het Bitmap object en tekent het in het clientvenster.

Het tekenwerk verwijderen uit de ui-thread

  1. Voeg in pch.h (stdafx.h in Visual Studio 2017 en eerder) de volgende #include instructies toe:

    #include <agents.h>
    #include <ppl.h>
    
  2. Voeg in ChildView.h de task_group en unbounded_buffer lidvariabelen toe aan de protected sectie van de CChildView klasse. Het task_group object bevat de taken die een tekening uitvoeren; het unbounded_buffer object bevat de voltooide Mandelbrot-afbeelding.

    concurrency::task_group m_DrawingTasks;
    concurrency::unbounded_buffer<BitmapPtr> m_MandelbrotImages;
    
  3. Voeg in ChildView.cpp een using instructie toe aan de concurrency naamruimte.

    using namespace concurrency;
    
  4. Roep in de CChildView::DrawMandelbrot-methode, na de aanroep van Bitmap::UnlockBits, de concurrency::send-functie aan om het Bitmap-object door te geven aan de UI-draad. Plaats vervolgens een verfbericht op de UI-thread en ongeldig het clientgebied.

    // 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. Werk de CChildView::OnPaint methode bij om het bijgewerkte Bitmap object te ontvangen en teken de afbeelding naar het clientvenster.

    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)));
          });
       }
    }
    

    Met de CChildView::OnPaint-methode creëer je een taak om de Mandelbrot-afbeelding te genereren, als deze nog niet in de berichtbuffer staat. De berichtbuffer bevat Bitmap geen object in gevallen zoals het eerste verfbericht en wanneer een ander venster vóór het clientvenster wordt verplaatst.

  6. Controleer of de toepassing is bijgewerkt door deze te bouwen en uit te voeren.

De gebruikersinterface reageert nu sneller omdat het tekenwerk op de achtergrond wordt uitgevoerd.

[Boven]

Prestaties van tekenen verbeteren

De generatie van de Mandelbrot fractal is een goede kandidaat voor parallellisatie omdat de berekening van elke pixel onafhankelijk is van alle andere berekeningen. Als u de tekenprocedure wilt parallelliseren, converteert u de buitenste for lus in de CChildView::DrawMandelbrot methode naar een aanroep naar het concurrency::parallel_for algoritme, als volgt.

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

Omdat de berekening van elk bitmapelement onafhankelijk is, hoeft u de tekenbewerkingen die toegang hebben tot het bitmapgeheugen niet te synchroniseren. Hierdoor kunnen de prestaties worden geschaald naarmate het aantal beschikbare processors toeneemt.

[Boven]

Ondersteuning voor annulering toevoegen

In deze sectie wordt beschreven hoe u het formaat van vensters kunt aanpassen en hoe u actieve tekentaken annuleert wanneer het venster wordt vernietigd.

In het document Annulering in de PPL wordt uitgelegd hoe annulering werkt in de runtime. Annulering is coöperatief; daarom gebeurt het niet onmiddellijk. Als u een geannuleerde taak wilt stoppen, genereert de runtime een interne uitzondering tijdens een volgende aanroep van de taak in de runtime. In de vorige sectie ziet u hoe u het parallel_for algoritme gebruikt om de prestaties van de tekentaak te verbeteren. De aanroep van parallel_for stelt de runtime in staat de taak te stoppen en maakt daardoor annulering mogelijk.

Actieve taken annuleren

De Mandelbrot-toepassing maakt Bitmap objecten waarvan de afmetingen overeenkomen met de grootte van het clientvenster. Telkens wanneer het formaat van het clientvenster wordt gewijzigd, maakt de toepassing een extra achtergrondtaak om een afbeelding te genereren voor de nieuwe venstergrootte. Voor de toepassing zijn deze tussenliggende afbeeldingen niet vereist; alleen de afbeelding voor de uiteindelijke venstergrootte is vereist. Om te voorkomen dat de toepassing dit extra werk uitvoert, kunt u alle actieve tekentaken annuleren in de berichtenhandlers voor de WM_SIZE- en WM_SIZING-berichten en vervolgens het tekenwerk opnieuw inplannen nadat het venster is verkleind of vergroot.

Als u actieve tekentaken wilt annuleren wanneer het venster wordt aangepast, roept de toepassing de concurrency::task_group::cancel-methode aan in de handlers voor de WM_SIZING en WM_SIZE berichten. De verwerker voor het WM_SIZE bericht roept ook de concurrency::task_group::wait-methode op om te wachten tot alle actieve taken zijn voltooid en plant vervolgens de tekentaak opnieuw in voor de bijgewerkte venstergrootte.

Wanneer het clientvenster wordt vernietigd, is het raadzaam om actieve tekentaken te annuleren. Als u actieve tekentaken annuleert, zorgt u ervoor dat worker threads geen berichten naar de UI-thread sturen nadat het clientvenster is vernietigd. De toepassing annuleert actieve tekentaken in de handler voor het WM_DESTROY bericht.

Reageren op annulering

De CChildView::DrawMandelbrot methode, waarmee de tekentaak wordt uitgevoerd, moet reageren op annulering. Omdat de runtime uitzonderingsafhandeling gebruikt om taken te annuleren, moet de CChildView::DrawMandelbrot methode een mechanisme voor uitzonderingsveilig gebruiken om te garanderen dat alle resources correct worden opgeschoond. In dit voorbeeld wordt het RAII-patroon ( Resource Acquisition Is Initialization ) gebruikt om te garanderen dat de bitmap-bits worden ontgrendeld wanneer de taak wordt geannuleerd.

Ondersteuning voor annulering toevoegen in de Mandelbrot-toepassing
  1. Voeg in ChildView.h, in de protected sectie van de CChildView klasse, declaraties toe voor de OnSize, OnSizing, en OnDestroy berichttoewijzingsfuncties.

    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. Wijzig in ChildView.cpp de berichttoewijzing zodat deze handlers voor de WM_SIZE, WM_SIZINGen WM_DESTROY berichten bevat.

    BEGIN_MESSAGE_MAP(CChildView, CWnd)
       ON_WM_PAINT()
       ON_WM_SIZE()
       ON_WM_SIZING()
       ON_WM_DESTROY()
    END_MESSAGE_MAP()
    
  3. Implementeer de methode CChildView::OnSizing. Met deze methode worden alle bestaande tekentaken geannuleerd.

    void CChildView::OnSizing(UINT nSide, LPRECT lpRect)
    {
       // The window size is changing; cancel any existing drawing tasks.
       m_DrawingTasks.cancel();
    }
    
  4. Implementeer de methode CChildView::OnSize. Met deze methode worden alle bestaande tekentaken geannuleerd en wordt een nieuwe tekentaak gemaakt voor de bijgewerkte grootte van het clientvenster.

    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. Implementeer de methode CChildView::OnDestroy. Met deze methode worden alle bestaande tekentaken geannuleerd.

    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. Definieer in ChildView.cpp de scope_guard klasse, waarmee het RAII-patroon wordt geïmplementeerd.

    // 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. Voeg de volgende code toe aan de CChildView::DrawMandelbrot methode na de aanroep naar 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);      
    });
    

    Deze code verwerkt annulering door een scope_guard object te maken. Wanneer het object het bereik verlaat, ontgrendelt het de bitmap-bits.

  8. Wijzig het einde van de CChildView::DrawMandelbrot methode om het scope_guard object te sluiten nadat de bitmap-bits zijn ontgrendeld, maar voordat berichten naar de UI-thread worden verzonden. Dit zorgt ervoor dat de UI-thread niet wordt bijgewerkt voordat de bitmap-bits worden ontgrendeld.

    // 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. Controleer of de toepassing is bijgewerkt door deze te bouwen en uit te voeren.

Wanneer u het formaat van het venster wijzigt, wordt tekenwerk alleen uitgevoerd voor de uiteindelijke venstergrootte. Actieve tekentaken worden ook geannuleerd wanneer het venster wordt vernietigd.

[Boven]

Zie ook

Gelijktijdigheidsruntime-handleidingen
Parallelle uitvoering van taken
Asynchrone berichtblokken
Functies voor het doorgeven van berichten
Parallelle algoritmen
Annulering in de PPL
MFC Desktop-toepassingen