Dela via


Vägledning: Ta bort arbetsuppgifter från en User-Interface-tråd

Det här dokumentet visar hur du använder Concurrency Runtime för att flytta det arbete som utförs av användargränssnittstråden (UI) i ett MFC-program (Microsoft Foundation Classes) till en arbetstråd. Det här dokumentet visar också hur du kan förbättra prestandan för en lång ritningsåtgärd.

Om du tar bort arbete från användargränssnittstråden genom att avlasta blockeringsåtgärder, till exempel ritning till arbetstrådar, kan du förbättra programmets svarstider. Den här genomgången använder en ritningsrutin som genererar Mandelbrot-fraktalen för att demonstrera en lång blockeringsåtgärd. Genereringen av mandelbrot fractal är också en bra kandidat för parallellisering eftersom beräkningen av varje pixel är oberoende av alla andra beräkningar.

Förutsättningar

Läs följande avsnitt innan du påbörjar den här genomgången:

Vi rekommenderar också att du förstår grunderna i MFC-programutveckling och GDI+ innan du påbörjar den här genomgången. Mer information om MFC finns i MFC Desktop-program. Mer information om GDI+ finns i GDI+.

Sektioner

Den här genomgången innehåller följande avsnitt:

Skapa MFC-programmet

I det här avsnittet beskrivs hur du skapar det grundläggande MFC-programmet.

Så här skapar du ett Visual C++ MFC-program

  1. Använd MFC-programguiden för att skapa ett MFC-program med alla standardinställningar. Mer information om hur du öppnar guiden för din version av Visual Studio finns i Genomgång: Använda de nya MFC Shell-kontrollerna .

  2. Skriv ett namn för projektet, till exempel Mandelbrot, och klicka sedan på OK för att visa MFC-programguiden.

  3. I fönstret Programtyp väljer du Enskilt dokument. Kontrollera att kryssrutan Stöd för dokument-/vyarkitektur är avmarkerad.

  4. Klicka på Slutför för att skapa projektet och stäng MFC-programguiden.

    Kontrollera att programmet har skapats framgångsrikt genom att bygga och köra det. Om du vill skapa programmet går du till menyn Skapa och klickar på Skapa lösning. Om programmet har skapats framgångsrikt, kör du programmet genom att klicka på Starta felsökningfelsökningsmenyn.

Implementera serieversionen av Mandelbrot-programmet

I det här avsnittet beskrivs hur du ritar mandelbrot-fraktalen. Den här versionen ritar Mandelbrot-fraktalen till ett GDI+ Bitmap-objekt och kopierar sedan innehållet i bitmappen till klientfönstret.

Implementera serieversionen av Mandelbrot-programmet

  1. Lägg till följande direktiv i pch.h (#include i Visual Studio 2017 och tidigare):

    #include <memory>
    
  2. Definiera pragma-typen efter BitmapPtr-direktivet i ChildView.h. Typen BitmapPtr gör att en pekare till ett Bitmap objekt kan delas av flera komponenter. Objektet Bitmap tas bort när det inte längre refereras till av någon komponent.

    typedef std::shared_ptr<Gdiplus::Bitmap> BitmapPtr;
    
  3. I ChildView.h lägger du till följande kod i protected avsnittet i CChildView klassen:

    protected:
       // Draws the Mandelbrot fractal to the specified Bitmap object.
       void DrawMandelbrot(BitmapPtr);
    
    protected:
       ULONG_PTR m_gdiplusToken;
    
  4. Kommentera ut eller ta bort följande rader i ChildView.cpp.

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

    I Felsökningsversioner förhindrar det här steget att programmet använder DEBUG_NEW allokeraren, som är inkompatibel med GDI+.

  5. I ChildView.cpp lägger du till ett using direktiv i Gdiplus namnområdet.

    using namespace Gdiplus;
    
  6. Lägg till följande kod i konstruktorn och destruktören för CChildView klassen för att initiera och stänga av GDI+.

    CChildView::CChildView()
    {
       // Initialize GDI+.
       GdiplusStartupInput gdiplusStartupInput;
       GdiplusStartup(&m_gdiplusToken, &gdiplusStartupInput, NULL);
    }
    
    CChildView::~CChildView()
    {
       // Shutdown GDI+.
       GdiplusShutdown(m_gdiplusToken);
    }
    
  7. Implementera metoden CChildView::DrawMandelbrot. Den här metoden ritar mandelbrot-fraktalen till det angivna Bitmap objektet.

    // 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. Implementera metoden CChildView::OnPaint. Den här metoden anropar CChildView::DrawMandelbrot och kopierar sedan innehållet i Bitmap objektet till fönstret.

    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. Kontrollera att programmet har uppdaterats genom att skapa och köra det.

Följande bild visar resultatet av Mandelbrot-programmet.

Mandelbrot-programmet.

Eftersom beräkningen för varje pixel är beräkningsmässigt dyr kan användargränssnittstråden inte bearbeta ytterligare meddelanden förrän den övergripande beräkningen har slutförts. Detta kan minska svarstiden i programmet. Du kan dock lösa det här problemet genom att ta bort arbete från användargränssnittstråden.

[Topp]

Ta bort arbete från användargränssnittstråden

Det här avsnittet visar hur du tar bort ritningsarbetet från användargränssnittstråden i Mandelbrot-programmet. Genom att flytta ritningsarbete från användargränssnittstråden till en arbetstråd kan användargränssnittstråden bearbeta meddelanden när arbetstråden genererar bilden i bakgrunden.

Concurrency Runtime innehåller tre sätt att köra uppgifter: aktivitetsgrupper, asynkrona agenter och enkla uppgifter. Även om du kan använda någon av dessa mekanismer för att ta bort arbete från användargränssnittstråden använder det här exemplet ett samtidighetsobjekt::task_group eftersom aktivitetsgrupper stöder annullering. Den här genomgången använder senare annullering för att minska mängden arbete som utförs när klientfönstret ändras och för att utföra rensning när fönstret förstörs.

Det här exemplet använder också ett samtidighet::unbounded_buffer-objekt för att aktivera användargränssnittstråden och arbetstråden för att kommunicera med varandra. När arbetstråden har skapat bilden skickar den en pekare till Bitmap objektet till unbounded_buffer objektet och postar sedan ett rederingsmeddelande till UI-tråden. Användargränssnittstråden tar sedan emot unbounded_buffer objektet från Bitmap objektet och ritar det i klientfönstret.

Ta bort ritningsarbetet från användargränssnittstråden

  1. I pch.h (stdafx.h i Visual Studio 2017 och tidigare) lägger du till följande #include direktiv:

    #include <agents.h>
    #include <ppl.h>
    
  2. I ChildView.h lägger du till task_group och unbounded_buffer medlemsvariabler i protected avsnittet i CChildView klassen. Objektet task_group innehåller de uppgifter som utför ritningen. unbounded_buffer Objektet innehåller den slutförda Mandelbrot-bilden.

    concurrency::task_group m_DrawingTasks;
    concurrency::unbounded_buffer<BitmapPtr> m_MandelbrotImages;
    
  3. I ChildView.cpp lägger du till ett using direktiv i concurrency namnområdet.

    using namespace concurrency;
    
  4. CChildView::DrawMandelbrot I -metoden anropar du funktionen Bitmap::UnlockBits efter anropet till för att skicka Bitmap objektet till användargränssnittstråden. Publicera sedan ett färgmeddelande i användargränssnittstråden och ogiltigförklara klientområdet.

    // 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. CChildView::OnPaint Uppdatera metoden för att ta emot det uppdaterade Bitmap objektet och dra avbildningen till klientfönstret.

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

    Metoden CChildView::OnPaint skapar en uppgift för att generera Mandelbrot-avbildningen om det inte finns någon i meddelandebufferten. Meddelandebufferten innehåller inte ett Bitmap objekt i fall som det första färgmeddelandet och när ett annat fönster flyttas framför klientfönstret.

  6. Kontrollera att programmet har uppdaterats genom att skapa och köra det.

Användargränssnittet är nu mer dynamiskt eftersom ritningsarbetet utförs i bakgrunden.

[Topp]

Förbättra ritningsprestanda

Genereringen av mandelbrot fractal är en bra kandidat för parallellisering eftersom beräkningen av varje pixel är oberoende av alla andra beräkningar. Om du vill parallellisera ritningsproceduren konverterar du den yttre for-loopen i CChildView::DrawMandelbrot-metoden till ett anrop till algoritmen concurrency::parallel_for, enligt följande.

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

Eftersom beräkningen av varje bitmappselement är oberoende behöver du inte synkronisera de ritningsåtgärder som har åtkomst till bitmappsminnet. Detta gör att prestanda kan skalas när antalet tillgängliga processorer ökar.

[Topp]

Lägga till stöd för annullering

I det här avsnittet beskrivs hur du hanterar storleksändring av fönster och hur du avbryter aktiva ritningsuppgifter när fönstret förstörs.

Dokumentet Annullering i PPL förklarar hur annulleringen fungerar under körningen. Annulleringen är samarbetsinriktad; därför sker det inte omedelbart. För att stoppa en avbruten arbetsuppgift utlöser runtime-miljön ett internt undantag under ett efterföljande anrop från arbetsuppgiften till runtime-miljön. I föregående avsnitt visas hur du använder algoritmen parallel_for för att förbättra ritningsuppgiftens prestanda. Anropet till parallel_for gör det möjligt för körningen att stoppa uppgiften och gör det därför möjligt att avbryta arbetet.

Avbryta aktiva aktiviteter

Mandelbrot-programmet skapar Bitmap objekt vars dimensioner matchar storleken på klientfönstret. Varje gång klientfönstret ändras skapar programmet ytterligare en bakgrundsaktivitet för att generera en avbildning för den nya fönsterstorleken. Programmet kräver inte dessa mellanliggande avbildningar. det kräver bara avbildningen för den slutliga fönsterstorleken. Om du vill förhindra att programmet utför det här extra arbetet kan du avbryta alla aktiva ritningsuppgifter i meddelandehanterare för WM_SIZE- och WM_SIZING-meddelanden och schemalägga om ritningen när fönstret har ändrats storlek.

För att avbryta aktiva ritningsuppgifter när fönstret ändras, anropar programmet concurrency::task_group::cancel-metoden i hanterarna för meddelandena och WM_SIZING. Hanteraren för WM_SIZE-meddelandet anropar också concurrency::task_group::wait-metoden för att vänta tills alla aktiva uppgifter har slutförts, och schemalägger sedan om ritningsaktiviteten för den uppdaterade fönsterstorleken.

När klientfönstret förstörs är det bra att avbryta alla aktiva ritningsuppgifter. Om du avbryter aktiva ritningsuppgifter ser du till att arbetstrådarna inte skickar meddelanden till användargränssnittstråden när klientfönstret har förstörts. Programmet avbryter alla pågående ritningsarbetsuppgifter i hanteraren för meddelandet WM_DESTROY.

Svara på annullering

Metoden CChildView::DrawMandelbrot , som utför ritningsuppgiften, måste svara på annulleringen. Eftersom körtiden använder undantagshantering för att avbryta uppgifter CChildView::DrawMandelbrot måste metoden använda en mekanism som är säker för undantag för att garantera att alla resurser frigörs korrekt. I det här exemplet används mönstret Resource Acquisition Is Initialization (RAII) för att garantera att bitmappsbitarna låss upp när uppgiften avbryts.

Lägga till stöd för annullering i Mandelbrot-programmet
  1. I ChildView.h, i protected-avsnittet av CChildView-klassen, lägg till deklarationer för meddelandekartfunktionerna OnSize, OnSizing och 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. I ChildView.cpp ändrar du meddelandekartan så att den innehåller hanterare för meddelandena WM_SIZE, WM_SIZINGoch .WM_DESTROY

    BEGIN_MESSAGE_MAP(CChildView, CWnd)
       ON_WM_PAINT()
       ON_WM_SIZE()
       ON_WM_SIZING()
       ON_WM_DESTROY()
    END_MESSAGE_MAP()
    
  3. Implementera metoden CChildView::OnSizing. Den här metoden avbryter alla befintliga ritningsuppgifter.

    void CChildView::OnSizing(UINT nSide, LPRECT lpRect)
    {
       // The window size is changing; cancel any existing drawing tasks.
       m_DrawingTasks.cancel();
    }
    
  4. Implementera metoden CChildView::OnSize. Den här metoden avbryter alla befintliga ritningsuppgifter och skapar en ny ritningsaktivitet för den uppdaterade klientfönstrets storlek.

    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. Implementera metoden CChildView::OnDestroy. Den här metoden avbryter alla befintliga ritningsuppgifter.

    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. I ChildView.cpp definierar du scope_guard klassen, som implementerar RAII-mönstret.

    // 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. Lägg till följande kod i CChildView::DrawMandelbrot metoden efter anropet till 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);      
    });
    

    Den här koden hanterar annullering genom att skapa ett scope_guard objekt. När objektet lämnar omfånget, låser det upp bitmapbitarna.

  8. Ändra slutet av CChildView::DrawMandelbrot metoden för att stänga scope_guard objektet när bitmappsbitarna har låsts upp, men innan några meddelanden skickas till användargränssnittstråden. Detta säkerställer att UI-tråden inte uppdateras innan bitmappsbitarna låses upp.

    // 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. Kontrollera att programmet har uppdaterats genom att skapa och köra det.

När du ändrar storlek på fönstret utförs ritningsarbete endast för den slutliga fönsterstorleken. Alla aktiva ritningsuppgifter avbryts också när fönstret förstörs.

[Topp]

Se även

Genomgång av samtidighetskörning
Uppgiftsparallellitet
Asynkrona meddelandeblock
Funktioner för meddelandeöverföring
Parallella algoritmer
Annullering i PPL
MFC Desktop-program