Exemplarische Vorgehensweise: Entfernen von Arbeit aus einem Benutzeroberflächenthread

In diesem Dokument wird veranschaulicht, wie Sie die Concurrency Runtime verwenden, um die Arbeit zu verschieben, die vom Benutzeroberflächenthread in einer Microsoft Foundation Classes (MFC)-Anwendung in einen Arbeitsthread ausgeführt wird. In diesem Dokument wird auch veranschaulicht, wie Sie die Leistung eines langwierigen Zeichnungsvorgangs verbessern.

Das Entfernen von Arbeiten aus dem UI-Thread durch Das Deaktivieren von Blockierungsvorgängen, z. B. Zeichnen, an Arbeitsthreads kann die Reaktionsfähigkeit Ihrer Anwendung verbessern. In dieser exemplarischen Vorgehensweise wird eine Zeichnungsroutine verwendet, die das Mandelbrot-Fractal generiert, um einen langwierigen Blockierungsvorgang zu veranschaulichen. Die Generierung des Mandelbrot-Fraktals ist auch ein guter Kandidat für parallele Parallelisierung, da die Berechnung jedes Pixels unabhängig von allen anderen Berechnungen ist.

Voraussetzungen

Lesen Sie sich folgende Themen durch, bevor Sie mit dieser exemplarischen Vorgehensweise beginnen:

Außerdem wird empfohlen, die Grundlagen der MFC-Anwendungsentwicklung und GDI+ zu verstehen, bevor Sie mit dieser exemplarischen Vorgehensweise beginnen. Weitere Informationen zu MFC finden Sie unter MFC-Desktopanwendungen. Weitere Informationen zu GDI+ finden Sie unter GDI+.

Abschnitte

Diese exemplarische Vorgehensweise enthält folgende Abschnitte:

Erstellen der MFC-Anwendung

In diesem Abschnitt wird beschrieben, wie Sie die grundlegende MFC-Anwendung erstellen.

So erstellen Sie eine Visual C++-MFC-Anwendung

  1. Verwenden Sie den MFC-Anwendungs-Assistenten , um eine MFC-Anwendung mit allen Standardeinstellungen zu erstellen. Siehe Exemplarische Vorgehensweise: Verwenden der neuen MFC-Shellsteuerelemente für Anweisungen zum Öffnen des Assistenten für Ihre Version von Visual Studio.

  2. Geben Sie einen Namen für das Projekt ein, z. B. , und klicken Sie dann auf "OK", Mandelbrotum den MFC-Anwendungs-Assistenten anzuzeigen.

  3. Wählen Sie im Bereich "Anwendungstyp " die Option "Einzelnes Dokument" aus. Stellen Sie sicher, dass das Kontrollkästchen " Dokument/Ansichtsarchitektur" deaktiviert ist.

  4. Klicken Sie auf "Fertig stellen ", um das Projekt zu erstellen und den MFC-Anwendungs-Assistenten zu schließen.

    Überprüfen Sie, ob die Anwendung erfolgreich erstellt wurde, indem Sie sie erstellen und ausführen. Klicken Sie zum Erstellen der Anwendung im Menü "Erstellen " auf " Projektmappe erstellen". Wenn die Anwendung erfolgreich erstellt wird, führen Sie die Anwendung aus, indem Sie im Menü "Debuggen" auf "Debuggen starten" klicken.

Implementierung der Serienversion der Mandelbrot-Anwendung

In diesem Abschnitt wird beschrieben, wie das Mandelbrot-Fraktal gezeichnet wird. Diese Version zeichnet das Mandelbrot-Fractal auf ein GDI+ Bitmap-Objekt und kopiert dann den Inhalt dieser Bitmap in das Clientfenster.

So implementieren Sie die Serienversion der Mandelbrot-Anwendung

  1. Fügen Sie in pch.h (stdafx.h in Visual Studio 2017 und früher) die folgende #include Direktive hinzu:

    #include <memory>
    
  2. Definieren Sie in ChildView.h nach der pragma Direktive den BitmapPtr Typ. Der BitmapPtr Typ ermöglicht es einem Zeiger auf ein Bitmap Objekt, von mehreren Komponenten gemeinsam genutzt zu werden. Das Bitmap Objekt wird gelöscht, wenn es nicht mehr von einer Komponente referenziert wird.

    typedef std::shared_ptr<Gdiplus::Bitmap> BitmapPtr;
    
  3. Fügen Sie in ChildView.h dem protected Abschnitt der CChildView Klasse den folgenden Code hinzu:

    protected:
       // Draws the Mandelbrot fractal to the specified Bitmap object.
       void DrawMandelbrot(BitmapPtr);
    
    protected:
       ULONG_PTR m_gdiplusToken;
    
  4. Kommentieren Oder entfernen Sie in ChildView.cpp die folgenden Zeilen.

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

    In Debugbuilds verhindert dieser Schritt, dass die Anwendung den DEBUG_NEW Allocator verwendet, der nicht mit GDI+ kompatibel ist.

  5. Fügen Sie in ChildView.cpp dem Gdiplus Namespace eine using Direktive hinzu.

    using namespace Gdiplus;
    
  6. Fügen Sie dem Konstruktor und destruktor der CChildView Klasse den folgenden Code hinzu, um GDI+ zu initialisieren und herunterzufahren.

    CChildView::CChildView()
    {
       // Initialize GDI+.
       GdiplusStartupInput gdiplusStartupInput;
       GdiplusStartup(&m_gdiplusToken, &gdiplusStartupInput, NULL);
    }
    
    CChildView::~CChildView()
    {
       // Shutdown GDI+.
       GdiplusShutdown(m_gdiplusToken);
    }
    
  7. Implementieren Sie die CChildView::DrawMandelbrot-Methode. Diese Methode zeichnet das Mandelbrot-Fractal auf das angegebene Bitmap Objekt.

    // 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. Implementieren Sie die CChildView::OnPaint-Methode. Diese Methode ruft CChildView::DrawMandelbrot den Inhalt des Bitmap Objekts in das Fenster auf und kopiert sie dann.

    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. Überprüfen Sie, ob die Anwendung erfolgreich aktualisiert wurde, indem Sie sie erstellen und ausführen.

Die folgende Abbildung zeigt die Ergebnisse der Mandelbrot-Anwendung.

The Mandelbrot Application.

Da die Berechnung für jedes Pixel rechenintensiv ist, kann der UI-Thread erst dann zusätzliche Meldungen verarbeiten, wenn die Gesamtberechnung abgeschlossen ist. Dies könnte die Reaktionsfähigkeit in der Anwendung verringern. Sie können dieses Problem jedoch lindern, indem Sie Die Arbeit aus dem UI-Thread entfernen.

[Nach oben]

Entfernen von Arbeit aus dem UI-Thread

In diesem Abschnitt wird gezeigt, wie Sie die Zeichnungsarbeit aus dem UI-Thread in der Mandelbrot-Anwendung entfernen. Durch das Verschieben von Zeichenarbeiten vom UI-Thread in einen Arbeitsthread kann der UI-Thread Nachrichten verarbeiten, während der Arbeitsthread das Bild im Hintergrund generiert.

Die Parallelitätslaufzeit bietet drei Möglichkeiten zum Ausführen von Aufgaben: Aufgabengruppen, asynchrone Agents und einfache Aufgaben. Obwohl Sie einen dieser Mechanismen verwenden können, um Arbeit aus dem UI-Thread zu entfernen, verwendet dieses Beispiel eine Parallelität::task_group-Objekt , da Aufgabengruppen den Abbruch unterstützen. In dieser exemplarischen Vorgehensweise wird später der Abbruch verwendet, um die Arbeitsmenge zu verringern, die beim Ändern der Größe des Clientfensters ausgeführt wird, und um sauber up auszuführen, wenn das Fenster zerstört wird.

In diesem Beispiel wird auch ein Parallelitätsobjekt::unbounded_buffer verwendet, um den UI-Thread und den Workerthread für die Kommunikation miteinander zu aktivieren. Nachdem der Arbeitsthread das Bild erzeugt hat, sendet er einen Zeiger auf das Bitmap Objekt an das unbounded_buffer Objekt und sendet dann eine Paint-Nachricht an den UI-Thread. Der UI-Thread empfängt dann vom Objekt das unbounded_bufferBitmap Objekt und zeichnet es in das Clientfenster.

So entfernen Sie die Zeichnungsarbeit aus dem UI-Thread

  1. Fügen Sie in pch.h (stdafx.h in Visual Studio 2017 und früher) die folgenden #include Direktiven hinzu:

    #include <agents.h>
    #include <ppl.h>
    
  2. Fügen Sie in ChildView.h dem protected Abschnitt der CChildView Klasse Variablen hinzu task_group und unbounded_buffer Member. Das task_group Objekt enthält die Aufgaben, die zeichnen; das unbounded_buffer Objekt enthält das fertige Mandelbrot-Bild.

    concurrency::task_group m_DrawingTasks;
    concurrency::unbounded_buffer<BitmapPtr> m_MandelbrotImages;
    
  3. Fügen Sie in ChildView.cpp dem concurrency Namespace eine using Direktive hinzu.

    using namespace concurrency;
    
  4. Rufen Sie in der CChildView::DrawMandelbrot Methode nach dem Aufruf Bitmap::UnlockBitsvon "concurrency::send" die Funktion auf, um das Bitmap Objekt an den UI-Thread zu übergeben. Posten Sie dann eine Paint-Nachricht im UI-Thread, und ungültigieren Sie den Clientbereich.

    // 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. Aktualisieren Sie die CChildView::OnPaint Methode, um das aktualisierte Bitmap Objekt zu empfangen und das Bild in das Clientfenster zu zeichnen.

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

    Die CChildView::OnPaint Methode erstellt eine Aufgabe, um das Mandelbrot-Bild zu generieren, wenn im Nachrichtenpuffer keins vorhanden ist. Der Nachrichtenpuffer enthält Bitmap kein Objekt in Fällen wie der anfänglichen Paint-Nachricht und wenn ein anderes Fenster vor das Clientfenster verschoben wird.

  6. Überprüfen Sie, ob die Anwendung erfolgreich aktualisiert wurde, indem Sie sie erstellen und ausführen.

Die Benutzeroberfläche ist jetzt reaktionsfähiger, da die Zeichenarbeit im Hintergrund ausgeführt wird.

[Nach oben]

Verbessern der Zeichenleistung

Die Generierung des Mandelbrot-Fraktals ist ein guter Kandidat für die Parallelisierung, da die Berechnung jedes Pixels unabhängig von allen anderen Berechnungen ist. Um die Zeichnungsprozedur zu parallelisieren, konvertieren Sie die äußere for Schleife in der CChildView::DrawMandelbrot Methode wie folgt in einen Aufruf der Parallelität::p arallel_for-Algorithmus .

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

Da die Berechnung der einzelnen Bitmapelemente unabhängig ist, müssen Sie die Zeichnungsvorgänge, die auf den Bitmapspeicher zugreifen, nicht synchronisieren. Dadurch kann die Leistung skaliert werden, da die Anzahl der verfügbaren Prozessoren zunimmt.

[Nach oben]

Hinzufügen von Support für Stornierung

In diesem Abschnitt wird beschrieben, wie Sie die Fenstergröße ändern und wie Sie aktive Zeichnungsaufgaben abbrechen, wenn das Fenster zerstört wird.

Der Dokumentabbruch in der PPL erläutert, wie der Abbruch in der Laufzeit funktioniert. Stornierung ist kooperativ; daher tritt sie nicht sofort auf. Um einen abgebrochenen Vorgang zu beenden, löst die Laufzeit eine interne Ausnahme während eines nachfolgenden Aufrufs der Aufgabe in die Laufzeit aus. Im vorherigen Abschnitt wird gezeigt, wie Sie den parallel_for Algorithmus verwenden, um die Leistung der Zeichnungsaufgabe zu verbessern. Der Aufruf ermöglicht parallel_for es der Laufzeit, die Aufgabe zu beenden, und ermöglicht daher das Funktionieren des Abbruchs.

Abbrechen aktiver Vorgänge

Die Mandelbrot-Anwendung erstellt Bitmap Objekte, deren Abmessungen der Größe des Clientfensters entsprechen. Jedes Mal, wenn die Größe des Clientfensters geändert wird, erstellt die Anwendung eine zusätzliche Hintergrundaufgabe, um ein Bild für die neue Fenstergröße zu generieren. Für die Anwendung sind diese Zwischenbilder nicht erforderlich; es erfordert nur das Bild für die endgültige Fenstergröße. Um zu verhindern, dass die Anwendung diese zusätzliche Arbeit ausführt, können Sie alle aktiven Zeichnungsaufgaben in den Nachrichtenhandlern für die WM_SIZE Nachrichten und WM_SIZING Nachrichten abbrechen und dann die Zeichnungsarbeit neu planen, nachdem die Größe des Fensters geändert wurde.

Um aktive Zeichnungsaufgaben abzubrechen, wenn die Größe des Fensters geändert wird, ruft die Anwendung die Parallelität::task_group::cancel-Methode in den Handlern für die WM_SIZING und WM_SIZE die Nachrichten auf. Der Handler für die WM_SIZE Nachricht ruft auch die Parallelität::task_group::wait-Methode auf, bis alle aktiven Aufgaben abgeschlossen sind, und plant dann den Zeichnungsvorgang für die aktualisierte Fenstergröße neu.

Wenn das Clientfenster zerstört wird, empfiehlt es sich, alle aktiven Zeichnungsaufgaben abzubrechen. Durch das Abbrechen aktiver Zeichnungsaufgaben wird sichergestellt, dass Arbeitsthreads keine Nachrichten im UI-Thread veröffentlichen, nachdem das Clientfenster zerstört wurde. Die Anwendung bricht alle aktiven Zeichnungsaufgaben im Handler für die WM_DESTROY Nachricht ab.

Reaktion auf Absage

Die CChildView::DrawMandelbrot Methode, die die Zeichnungsaufgabe ausführt, muss auf den Abbruch reagieren. Da die Laufzeit die Ausnahmebehandlung zum Abbrechen von Vorgängen verwendet, muss die CChildView::DrawMandelbrot Methode einen ausnahmesicheren Mechanismus verwenden, um sicherzustellen, dass alle Ressourcen ordnungsgemäß sauber ed-up sind. In diesem Beispiel wird das RAII-Muster (Resource Acquisition Is Initialization ) verwendet, um sicherzustellen, dass die Bitmapbits entsperrt werden, wenn der Vorgang abgebrochen wird.

So fügen Sie Unterstützung für die Stornierung in der Mandelbrot-Anwendung hinzu
  1. Fügen Sie in ChildView.h im protected Abschnitt der CChildView Klasse Deklarationen für die OnSizeFunktionen " und OnSizingOnDestroy "Message Map" hinzu.

    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. Ändern Sie in ChildView.cpp die Nachrichtenzuordnung so, dass sie Handler für die WM_SIZE, WM_SIZINGund WM_DESTROY Nachrichten enthält.

    BEGIN_MESSAGE_MAP(CChildView, CWnd)
       ON_WM_PAINT()
       ON_WM_SIZE()
       ON_WM_SIZING()
       ON_WM_DESTROY()
    END_MESSAGE_MAP()
    
  3. Implementieren Sie die CChildView::OnSizing-Methode. Mit dieser Methode werden alle vorhandenen Zeichnungsaufgaben abgebrochen.

    void CChildView::OnSizing(UINT nSide, LPRECT lpRect)
    {
       // The window size is changing; cancel any existing drawing tasks.
       m_DrawingTasks.cancel();
    }
    
  4. Implementieren Sie die CChildView::OnSize-Methode. Mit dieser Methode werden alle vorhandenen Zeichnungsaufgaben abgebrochen und eine neue Zeichnungsaufgabe für die aktualisierte Clientfenstergröße erstellt.

    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. Implementieren Sie die CChildView::OnDestroy-Methode. Mit dieser Methode werden alle vorhandenen Zeichnungsaufgaben abgebrochen.

    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. Definieren Sie in ChildView.cpp die scope_guard Klasse, die das RAII-Muster implementiert.

    // 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. Fügen Sie der CChildView::DrawMandelbrot Methode nach dem Aufruf Bitmap::LockBitsvon :

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

    Dieser Code behandelt den Abbruch durch Erstellen eines scope_guard Objekts. Wenn das Objekt den Bereich verlässt, entsperrt es die Bitmapbits.

  8. Ändern Sie das Ende der CChildView::DrawMandelbrot Methode, um das scope_guard Objekt zu schließen, nachdem die Bitmapbits entsperrt wurden, aber bevor nachrichten an den UI-Thread gesendet werden. Dadurch wird sichergestellt, dass der UI-Thread nicht aktualisiert wird, bevor die Bitmapbits entsperrt werden.

    // 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. Überprüfen Sie, ob die Anwendung erfolgreich aktualisiert wurde, indem Sie sie erstellen und ausführen.

Wenn Sie die Größe des Fensters ändern, wird die Zeichnung nur für die endgültige Fenstergröße ausgeführt. Alle aktiven Zeichnungsaufgaben werden auch abgebrochen, wenn das Fenster zerstört wird.

[Nach oben]

Siehe auch

Exemplarische Vorgehensweisen für die Concurrency Runtime
Task-Parallelität
Asynchrone Nachrichtenblöcke
Funktionen zum Übergeben von Nachrichten
Parallele Algorithmen
Abbruch in der PPL
MFC-Desktopanwendungen