Share via


Exemplarische Vorgehensweise: Entfernen von Arbeit aus einem Benutzeroberflächenthread

Dieses Dokument veranschaulicht, wie Sie Verarbeitungsschritte, die in einer MFC-Anwendung (Microsoft Foundation Classes) im Benutzeroberflächenthread ausgeführt werden, mithilfe der Concurrency Runtime in einen Arbeitsthread verschieben können. Außerdem werden in diesem Dokument Möglichkeiten der Leistungsverbesserung bei aufwändigen Zeichenvorgängen beschrieben.

Durch das Auslagern blockierender Verarbeitungsschritte, z. B. für das Zeichnen, aus dem UI-Thread in Arbeitsthreads kann die Reaktionszeit der Anwendung verbessert werden. Diese exemplarische Vorgehensweise verwendet eine Zeichnungsroutine, die das Mandelbrot-Fraktal generiert, um einen aufwändigen blockierenden Vorgang zu veranschaulichen. Die Generierung des Mandelbrot-Fraktals ist außerdem gut für die Parallelisierung geeignet, da die Berechnung jedes Pixels von allen anderen Berechnungen unabhängig ist.

Vorbereitungsmaßnahmen

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

Es wird empfohlen, dass Sie sich auch mit den Grundlagen der MFC-Anwendungsentwicklung und mit GDI+ vertraut machen, bevor Sie mit dieser exemplarischen Vorgehensweise beginnen. Weitere Informationen über MFC finden Sie unter MFC Reference. Weitere Informationen zu GDI+ finden Sie unter GDI+.

Abschnitte

Diese exemplarische Vorgehensweise enthält folgende Abschnitte:

  • Erstellen der MFC-Anwendung

  • Implementieren der seriellen Version der Mandelbrot-Anwendung

  • Entfernen von Verarbeitungsschritten aus dem Benutzeroberflächenthread

  • Verbessern der Zeichenleistung

  • Hinzufügen von Unterstützung für Abbrüche

Erstellen der MFC-Anwendung

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

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

  1. Klicken Sie im Menü Datei auf Neu und dann auf Projekt.

  2. Wählen Sie im Dialogfeld Neues Projekt im Bereich Installierte Vorlagen den Typ Visual C++ aus, und klicken Sie dann im Bereich Vorlagen auf MFC-Anwendung. Geben Sie einen Namen für das Projekt ein, z. B. Mandelbrot, und klicken Sie dann auf OK, um den MFC-Anwendungs-Assistenten anzuzeigen.

  3. Wählen Sie im Bereich Anwendungstyp die Option Einfaches Dokument aus. Stellen Sie sicher, dass das Kontrollkästchen Unterstützung für die Dokument-/Ansichtarchitektur aktiviert 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 im Menü Erstellen auf Projektmappe erstellen, um die Anwendung zu erstellen. Wenn die Anwendung erfolgreich erstellt wird, führen Sie die Anwendung aus, indem Sie im Menü Debug auf Debugging starten klicken.

Implementieren der seriellen Version der Mandelbrot-Anwendung

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

So implementieren Sie die serielle Version der Mandelbrot-Anwendung

  1. Fügen Sie in der Datei stdafx.h 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, einen Zeiger auf ein Bitmap-Objekt in mehreren Komponenten gemeinsam zu verwenden. Das Bitmap-Objekt wird gelöscht, wenn keine Komponente mehr auf das Objekt verweist.

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

    protected:
       // Draws the Mandelbrot fractal to the specified Bitmap object.
       void DrawMandelbrot(BitmapPtr);
    
    protected:
       ULONG_PTR m_gdiplusToken;
    
  4. Löschen oder kommentieren Sie in ChildView.cpp folgende Zeilen aus.

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

    In Debugbuilds verhindert dieser Schritt, dass die Anwendung die DEBUG_NEW-Belegungsfunktion verwendet, die mit GDI+ nicht 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 dem Destruktor der CChildView-Klasse den folgenden Code zum Initialisieren und Schließen von GDI+ hinzu.

    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-Fraktal im angegebenen 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 auf und kopiert anschließend den Inhalt des Bitmap-Objekts in das Fenster.

    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 das Aktualisieren der Anwendung erfolgreich war, indem Sie sie erstellen und ausführen.

Die folgende Abbildung zeigt die Ausgabe der Mandelbrot-Anwendung.

Mandelbrot-Anwendung

Da die Berechnung für jedes Pixel rechenintensiv ist, kann der UI-Thread erst dann wieder andere Meldungen verarbeiten, wenn die gesamte Berechnung abgeschlossen ist. Dies kann die Reaktionszeit der Anwendung herabsetzen. Sie können dieses Problem jedoch vermeiden, indem Sie die Verarbeitung aus dem UI-Thread auslagern.

[Nach oben]

Auslagern der Verarbeitung aus dem UI-Thread

Dieser Abschnitt beschreibt, wie die Zeichenarbeit der Mandelbrot-Anwendung aus dem UI-Thread ausgelagert wird. Wenn die Zeichenarbeit aus dem UI-Thread in einen Arbeitsthread verschoben wird, kann der UI-Thread weiter Meldungen verarbeiten, während der Arbeitsthread im Hintergrund das Bild generiert.

Die Concurrency Runtime bietet drei Möglichkeiten, Aufgaben auszuführen: Aufgabengruppen, asynchrone Agents und einfache Aufgaben. Obwohl Sie jeden dieser Mechanismen verwenden können, um die Arbeit aus dem UI-Thread auszulagern, wird in diesem Beispiel ein Concurrency::task_group-Objekt verwendet, da Aufgabengruppen Abbrüche unterstützen. Im weiteren Verlauf dieser exemplarischen Vorgehensweise werden Abbrüche verwendet, um den Arbeitsaufwand bei Größenänderungen des Clientfensters zu verringern und um die Bereinigung durchzuführen, wenn das Fenster zerstört wird.

Das Beispiel verwendet außerdem ein Concurrency::unbounded_buffer-Objekt, über das der UI-Thread und der Arbeitsthread kommunizieren. Nachdem der Arbeitsthread das Bild erzeugt hat, sendet er einen Zeiger auf das Bitmap-Objekt an das unbounded_buffer-Objekt und gibt anschließend eine paint-Meldung an den UI-Thread aus. Der UI-Thread empfängt dann das Bitmap-Objekt vom unbounded_buffer-Objekt und zeichnet es im Clientfenster.

So lagern Sie die Zeichenarbeit aus dem UI-Thread aus

  1. Fügen Sie in der Datei stdafx.h folgende #include-Direktiven hinzu:

    #include <agents.h>
    #include <ppl.h>
    
  2. Fügen Sie in ChildView.h task_group- und unbounded_buffer-Membervariablen im protected-Abschnitt der CChildView-Klasse hinzu. Das task_group-Objekt enthält die Aufgaben, die das Zeichnen ausführen und das unbounded_buffer-Objekt enthält das abgeschlossene 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 von Bitmap::UnlockBits die Concurrency::send-Funktion auf, um das Bitmap-Objekt an den UI-Thread zu übergeben. Geben Sie dann eine paint-Meldung an den UI-Thread aus, und legen Sie den Clientbereich als ungültig fest.

    // 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 im 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 zum Generieren des Mandelbrot-Bilds, wenn im Meldungspuffer noch kein Bild vorhanden ist. Der Meldungspuffer enthält in verschiedenen Fällen kein Bitmap-Objekt, z. B. während der ersten paint-Meldung oder wenn ein anderes Fenster das Clientfenster verdeckt.

  6. Überprüfen Sie, ob das Aktualisieren der Anwendung erfolgreich war, indem Sie sie erstellen und ausführen.

Die Benutzeroberfläche reagiert jetzt schneller, da die Zeichenarbeit im Hintergrund ausgeführt wird.

[Nach oben]

Verbessern der Zeichenleistung

Die Generierung des Mandelbrot-Fraktals ist außerdem gut für die Parallelisierung geeignet, da die Berechnung jedes Pixels von allen anderen Berechnungen unabhängig ist. Um die Zeichnungsprozedur zu parallelisieren, konvertieren Sie die äußere for-Schleife in der CChildView::DrawMandelbrot-Methode wie folgt in einen Aufruf des Concurrency::parallel_for-Algorithmus.

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

Da jedes Bitmapelement unabhängig berechnet wird, müssen Sie die Zeichenvorgänge, die auf den Bitmaparbeitsspeicher zugreifen, nicht synchronisieren. Dadurch kann die Zeichenleistung in Abhängigkeit von der Anzahl der verfügbaren Prozessoren skalieren.

[Nach oben]

Hinzufügen von Unterstützung für Abbrüche

In diesem Abschnitt wird beschrieben, wie Änderungen der Fenstergröße behandelt und wie aktive Zeichenaufgaben abgebrochen werden können, wenn das Fenster zerstört wird.

Das Dokument Abbruch in der PPL erläutert, wie Abbrüche in der Laufzeit funktionieren. Abbrüche sind kooperativ und treten daher nicht sofort auf. Um eine abgebrochene Aufgabe zu beenden, löst die Laufzeit eine interne Ausnahme während eines nachfolgenden Aufrufs der Laufzeit durch die Aufgabe aus. Der vorherige Abschnitt zeigt, wie der parallel_for-Algorithmus zum Verbessern der Leistung der Zeichenaufgabe verwendet werden kann. Durch den Aufruf von parallel_for ist die Laufzeit in der Lage, die Aufgabe zu beenden und Abbrüche auszuführen.

Abbrechen von aktiven Aufgaben

Die Mandelbrot-Anwendung erstellt Bitmap-Objekte, deren Dimensionen 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. Die Anwendung benötigt diese Zwischenbilder nicht. Es wird nur das Bild für die abschließende Fenstergröße benötigt. Um zu verhindern, dass die Anwendung diese zusätzliche Arbeit durchführt, können Sie alle aktiven Zeichenaufgaben in den Meldungshandlern für die WM_SIZE- und die WM_SIZING-Meldung abbrechen und die Zeichenarbeit neu planen, sobald die endgültige Größe des Fensters feststeht.

Um aktive Zeichenaufgaben bei Änderungen der Fenstergröße abzubrechen, ruft die Anwendung in den Handlern für die WM_SIZING-Meldung und die WM_SIZE-Meldung die Concurrency::task_group::cancel-Methode auf. Der Handler für die WM_SIZE-Meldung ruft außerdem die Concurrency::task_group::wait-Methode auf, mit der auf den Abschluss aller aktiven Aufgaben gewartet wird. Anschließend wird die Zeichenaufgabe für die aktualisierte Fenstergröße neu geplant.

Wenn das Clientfenster zerstört wird, empfiehlt es sich, alle aktiven Zeichenaufgaben abzubrechen. Durch das Abbrechen aktiver Zeichenaufgaben wird sichergestellt, dass Arbeitsthreads keine Meldungen an den UI-Thread ausgeben, nachdem das Clientfenster zerstört wurde. Die Anwendung bricht alle aktiven Zeichenaufgaben im Handler für die WM_DESTROY-Meldung ab.

Reagieren auf Abbrüche

Die CChildView::DrawMandelbrot-Methode, die die Zeichenaufgabe ausführt, muss auf den Abbruch reagieren. Da die Laufzeit Aufgaben durch eine Ausnahmebehandlung abbricht, muss die CChildView::DrawMandelbrot-Methode einen ausnahmesicheren Mechanismus verwenden, um zu gewährleisten, dass alle Ressourcen ordnungsgemäß bereinigt werden. In diesem Beispiel wird das RAII (Resource Acquisition Is Initialization)-Muster verwendet, um zu gewährleisten, dass die Bitmapbits entsperrt werden, wenn die Aufgabe abgebrochen wird.

So fügen Sie die Unterstützung für Abbrüche in der Mandelbrot-Anwendung hinzu

  1. Fügen Sie in ChildView.h im protected-Abschnitt der CChildView-Klasse Deklarationen für die Meldungszuordnungsfunktionen OnSize, OnSizing und OnDestroy 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 Meldungszuordnung, sodass sie Handler für die Meldungen WM_SIZE, WM_SIZING und WM_DESTROY 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. Diese Methode bricht alle vorhandenen Zeichenaufgaben ab.

    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. Diese Methode bricht alle vorhandenen Zeichenaufgaben ab und erstellt eine neue Zeichenaufgabe für die aktualisierte Clientfenstergröße.

    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. Diese Methode bricht alle vorhandenen Zeichenaufgaben ab.

    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 folgenden Code in der CChildView::DrawMandelbrot-Methode nach dem Aufruf von Bitmap::LockBits hinzu.

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

    In diesem Code werden Abbrüche durch Erstellen eines scope_guard-Objekts behandelt. Wenn das Objekt den Gültigkeitsbereich verlässt, werden die Bitmapbits entsperrt.

  8. Ändern Sie das Ende der CChildView::DrawMandelbrot-Methode, sodass das scope_guard-Objekt verworfen wird, nachdem die Bitmapbits entsperrt wurden, aber bevor Meldungen an den UI-Thread gesendet werden. Hierdurch 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 das Aktualisieren der Anwendung erfolgreich war, indem Sie sie erstellen und ausführen.

Wenn Sie die Größe des Fensters ändern, wird die Zeichenarbeit nur für die abschließende Fenstergröße ausgeführt. Außerdem werden alle aktiven Zeichenaufgaben abgebrochen, wenn das Fenster zerstört wird.

[Nach oben]

Siehe auch

Konzepte

Exemplarische Vorgehensweisen für die Concurrency Runtime

Aufgabenparallelität (Concurrency Runtime)

Asynchrone Nachrichtenblöcke

Funktionen zum Übergeben von Meldungen

Parallele Algorithmen

Abbruch in der PPL

Weitere Ressourcen

MFC Reference