Udostępnij za pośrednictwem


Wskazówki: usuwanie pracy z wątku interfejs użytkownika

W tym dokumencie pokazano, jak za pomocą środowiska uruchomieniowego współbieżności przenieść pracę wykonywaną przez wątek interfejsu użytkownika w aplikacji Klasy programu Microsoft Foundation (MFC) do wątku roboczego. W tym dokumencie pokazano również, jak poprawić wydajność długiej operacji rysowania.

Usunięcie pracy z wątku interfejsu użytkownika przez odciążenie operacji blokujących, na przykład rysunek, do wątków roboczych może poprawić czas reakcji aplikacji. W tym przewodniku użyto procedury rysunku, która generuje fraktal Mandelbrot, aby zademonstrować długotrwałą operację blokowania. Generowanie fraktalu Mandelbrot jest również dobrym kandydatem do równoległości, ponieważ obliczanie każdego piksela jest niezależne od wszystkich innych obliczeń.

Wymagania wstępne

Przed rozpoczęciem tego przewodnika zapoznaj się z następującymi tematami:

Zalecamy również zapoznanie się z podstawami tworzenia aplikacji MFC i GDI+ przed rozpoczęciem tego przewodnika. Aby uzyskać więcej informacji na temat MFC, zobacz MFC Desktop Applications (Aplikacje klasyczne MFC). Aby uzyskać więcej informacji na temat interfejsu GDI+, zobacz GDI+.

Sekcje

Ten przewodnik zawiera następujące sekcje:

Tworzenie aplikacji MFC

W tej sekcji opisano sposób tworzenia podstawowej aplikacji MFC.

Aby utworzyć aplikację MFC języka Visual C++

  1. Użyj Kreatora aplikacji MFC, aby utworzyć aplikację MFC ze wszystkimi ustawieniami domyślnymi. Zobacz Przewodnik: używanie nowych kontrolek powłoki MFC, aby uzyskać instrukcje dotyczące otwierania kreatora dla używanej wersji programu Visual Studio.

  2. Wpisz nazwę projektu, na przykład , a następnie kliknij przycisk OK, Mandelbrotaby wyświetlić Kreatora aplikacji MFC.

  3. W okienku Typ aplikacji wybierz pozycję Pojedynczy dokument. Upewnij się, że pole wyboru Obsługa architektury dokumentu/widoku zostało wyczyszczone.

  4. Kliknij przycisk Zakończ , aby utworzyć projekt i zamknąć Kreatora aplikacji MFC.

    Sprawdź, czy aplikacja została utworzona pomyślnie, kompilując i uruchamiając ją. Aby skompilować aplikację, w menu Kompilacja kliknij pozycję Kompiluj rozwiązanie. Jeśli aplikacja zostanie pomyślnie skompilowany, uruchom aplikację, klikając pozycję Rozpocznij debugowanie w menu Debugowanie.

Implementowanie wersji szeregowej aplikacji Mandelbrot

W tej sekcji opisano sposób rysowania fraktu Mandelbrot. Ta wersja rysuje fraktal Mandelbrot do obiektu GDI+ Bitmap , a następnie kopiuje zawartość tej mapy bitowej do okna klienta.

Aby zaimplementować wersję szeregowej aplikacji Mandelbrot

  1. W pliku pch.h (stdafx.h w programie Visual Studio 2017 i starszych wersjach) dodaj następującą #include dyrektywę:

    #include <memory>
    
  2. W pliku ChildView.h po dyrektywie zdefiniuj pragma BitmapPtr typ. Typ BitmapPtr umożliwia udostępnianie przez wiele składników wskaźnika Bitmap do obiektu. Obiekt Bitmap jest usuwany, gdy nie odwołuje się już do niego żaden składnik.

    typedef std::shared_ptr<Gdiplus::Bitmap> BitmapPtr;
    
  3. W pliku ChildView.h dodaj następujący kod do protected sekcji CChildView klasy:

    protected:
       // Draws the Mandelbrot fractal to the specified Bitmap object.
       void DrawMandelbrot(BitmapPtr);
    
    protected:
       ULONG_PTR m_gdiplusToken;
    
  4. W ChildView.cpp oznacz jako komentarz lub usuń następujące wiersze.

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

    W obszarze Kompilacje debugowania ten krok uniemożliwia aplikacji korzystanie z alokatora DEBUG_NEW , który jest niezgodny z interfejsem GDI+.

  5. W ChildView.cpp dodaj dyrektywę using do Gdiplus przestrzeni nazw.

    using namespace Gdiplus;
    
  6. Dodaj następujący kod do konstruktora i destruktora CChildView klasy, aby zainicjować i zamknąć interfejs GDI+.

    CChildView::CChildView()
    {
       // Initialize GDI+.
       GdiplusStartupInput gdiplusStartupInput;
       GdiplusStartup(&m_gdiplusToken, &gdiplusStartupInput, NULL);
    }
    
    CChildView::~CChildView()
    {
       // Shutdown GDI+.
       GdiplusShutdown(m_gdiplusToken);
    }
    
  7. Zaimplementuj metodę CChildView::DrawMandelbrot . Ta metoda rysuje fraktal Mandelbrot do określonego Bitmap obiektu.

    // 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. Zaimplementuj metodę CChildView::OnPaint . Ta metoda wywołuje CChildView::DrawMandelbrot metodę , a następnie kopiuje zawartość Bitmap obiektu do okna.

    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. Sprawdź, czy aplikacja została pomyślnie zaktualizowana, kompilując i uruchamiając ją.

Poniższa ilustracja przedstawia wyniki aplikacji Mandelbrot.

Aplikacja Mandelbrot.

Ponieważ obliczenia dla każdego piksela są kosztowne obliczeniowo, wątek interfejsu użytkownika nie może przetwarzać dodatkowych komunikatów, dopóki ogólne obliczenia nie zakończą się. Może to zmniejszyć czas odpowiedzi w aplikacji. Ten problem można jednak rozwiązać, usuwając pracę z wątku interfejsu użytkownika.

[Top]

Usuwanie pracy z wątku interfejsu użytkownika

W tej sekcji pokazano, jak usunąć pracę rysunkową z wątku interfejsu użytkownika w aplikacji Mandelbrot. Przenosząc pracę rysunkową z wątku interfejsu użytkownika do wątku roboczego, wątek interfejsu użytkownika może przetwarzać komunikaty, gdy wątek procesu roboczego generuje obraz w tle.

Środowisko uruchomieniowe współbieżności udostępnia trzy sposoby uruchamiania zadań: grup zadań, agentów asynchronicznych i lekkich zadań. Chociaż można użyć dowolnego z tych mechanizmów, aby usunąć pracę z wątku interfejsu użytkownika, w tym przykładzie użyto obiektu współbieżności::task_group , ponieważ grupy zadań obsługują anulowanie. W tym przewodniku później użyto anulowania, aby zmniejszyć ilość pracy wykonywanej podczas zmiany rozmiaru okna klienta i wykonać oczyszczanie w przypadku zniszczenia okna.

W tym przykładzie użyto również obiektu współbieżności::unbounded_buffer , aby umożliwić wątkowi interfejsu użytkownika i wątkowi roboczemu komunikowanie się ze sobą. Gdy wątek procesu roboczego utworzy obraz, wysyła wskaźnik do obiektu do Bitmap unbounded_buffer obiektu, a następnie publikuje komunikat farby do wątku interfejsu użytkownika. Następnie wątek interfejsu unbounded_buffer użytkownika odbiera z obiektu Bitmap obiekt i rysuje go do okna klienta.

Aby usunąć pracę rysunkową z wątku interfejsu użytkownika

  1. W pliku pch.h (stdafx.h w programie Visual Studio 2017 i starszych wersjach) dodaj następujące #include dyrektywy:

    #include <agents.h>
    #include <ppl.h>
    
  2. W pliku ChildView.h dodaj task_group zmienne składowe i unbounded_buffer do protected sekcji CChildView klasy. Obiekt task_group przechowuje zadania, które wykonują rysunek; unbounded_buffer obiekt zawiera ukończony obraz Mandelbrot.

    concurrency::task_group m_DrawingTasks;
    concurrency::unbounded_buffer<BitmapPtr> m_MandelbrotImages;
    
  3. W ChildView.cpp dodaj dyrektywę using do concurrency przestrzeni nazw.

    using namespace concurrency;
    
  4. W metodzie po wywołaniu CChildView::DrawMandelbrot metody Bitmap::UnlockBitswywołaj metodę , wywołaj funkcję concurrency::send , aby przekazać Bitmap obiekt do wątku interfejsu użytkownika. Następnie opublikuj komunikat farby w wątku interfejsu użytkownika i unieważnij obszar klienta.

    // 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. Zaktualizuj metodę w celu odebrania CChildView::OnPaint zaktualizowanego Bitmap obiektu i narysuj obraz do okna klienta.

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

    Metoda CChildView::OnPaint tworzy zadanie generowania obrazu Mandelbrot, jeśli nie istnieje w buforze komunikatów. Bufor komunikatów nie będzie zawierać Bitmap obiektu w przypadkach, takich jak początkowy komunikat malowania i gdy inne okno zostanie przeniesione przed oknem klienta.

  6. Sprawdź, czy aplikacja została pomyślnie zaktualizowana, kompilując i uruchamiając ją.

Interfejs użytkownika jest teraz bardziej dynamiczny, ponieważ praca rysunkowa jest wykonywana w tle.

[Top]

Poprawianie wydajności rysunku

Generowanie fraktalu Mandelbrot jest dobrym kandydatem do równoległości, ponieważ obliczanie każdego piksela jest niezależne od wszystkich innych obliczeń. Aby zrównać procedurę rysunku, przekonwertuj pętlę zewnętrzną for w CChildView::DrawMandelbrot metodzie na wywołanie współbieżności::p arallel_for algorithm w następujący sposób.

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

Ponieważ obliczanie każdego elementu mapy bitowej jest niezależne, nie trzeba synchronizować operacji rysowania, które uzyskują dostęp do pamięci mapy bitowej. Umożliwia to skalowanie wydajności w miarę wzrostu liczby dostępnych procesorów.

[Top]

Dodawanie obsługi anulowania

W tej sekcji opisano sposób obsługi zmiany rozmiaru okna i sposobu anulowania jakichkolwiek aktywnych zadań rysunkowych w przypadku zniszczenia okna.

W dokumencie Anulowanie w PPL wyjaśniono, jak działa anulowanie w środowisku uruchomieniowym. Anulowanie jest spółdzielcze; w związku z tym nie występuje natychmiast. Aby zatrzymać anulowane zadanie, środowisko uruchomieniowe zgłasza wyjątek wewnętrzny podczas kolejnego wywołania zadania do środowiska uruchomieniowego. W poprzedniej sekcji pokazano, jak za pomocą algorytmu parallel_for poprawić wydajność zadania rysunkowego. Wywołanie w celu parallel_for umożliwienia środowisku uruchomieniowemu zatrzymania zadania i w związku z tym umożliwia anulowanie pracy.

Anulowanie aktywnych zadań

Aplikacja Mandelbrot tworzy Bitmap obiekty, których wymiary odpowiadają rozmiarowi okna klienta. Za każdym razem, gdy okno klienta zostanie zmieniony, aplikacja tworzy dodatkowe zadanie w tle w celu wygenerowania obrazu dla nowego rozmiaru okna. Aplikacja nie wymaga tych obrazów pośrednich; wymaga tylko obrazu dla końcowego rozmiaru okna. Aby zapobiec wykonywaniu tej dodatkowej pracy przez aplikację, możesz anulować wszystkie aktywne zadania rysunkowe w programach obsługi komunikatów dla WM_SIZE komunikatów i WM_SIZING , a następnie ponownie zaplanować pracę rysunku po zmianie rozmiaru okna.

Aby anulować aktywne zadania rysunkowe po zmianie rozmiaru okna, aplikacja wywołuje metodę concurrency::task_group::cancel w programach obsługi komunikatów WM_SIZING i WM_SIZE . Procedura obsługi komunikatu WM_SIZE wywołuje również metodę concurrency::task_group::wait , aby poczekać na ukończenie wszystkich aktywnych zadań, a następnie ponownie wyślij zadanie rysunku do zaktualizowanego rozmiaru okna.

Gdy okno klienta zostanie zniszczone, dobrym rozwiązaniem jest anulowanie jakichkolwiek aktywnych zadań rysunkowych. Anulowanie jakichkolwiek aktywnych zadań rysunkowych gwarantuje, że wątki robocze nie publikują komunikatów w wątku interfejsu użytkownika po zniszczeniu okna klienta. Aplikacja anuluje wszystkie aktywne zadania rysunkowe w procedurze obsługi komunikatu WM_DESTROY .

Odpowiadanie na anulowanie

Metoda CChildView::DrawMandelbrot , która wykonuje zadanie rysunku, musi odpowiadać na anulowanie. Ponieważ środowisko uruchomieniowe używa obsługi wyjątków do anulowania zadań, CChildView::DrawMandelbrot metoda musi używać mechanizmu bezpiecznego dla wyjątków, aby zagwarantować, że wszystkie zasoby są prawidłowo czyszczone. W tym przykładzie użyto wzorca Pozyskiwanie zasobów inicjalizacji (RAII), aby zagwarantować, że bity bitów bitów bitowych zostaną odblokowane po anulowaniu zadania.

Aby dodać obsługę anulowania w aplikacji Mandelbrot
  1. W pliku ChildView.h w protected sekcji CChildView klasy dodaj deklaracje dla funkcji mapy komunikatów OnSizei , i .OnDestroy OnSizing

    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. W ChildView.cpp zmodyfikuj mapę komunikatów tak, aby zawierała programy obsługi dla komunikatów WM_SIZE, WM_SIZINGi WM_DESTROY .

    BEGIN_MESSAGE_MAP(CChildView, CWnd)
       ON_WM_PAINT()
       ON_WM_SIZE()
       ON_WM_SIZING()
       ON_WM_DESTROY()
    END_MESSAGE_MAP()
    
  3. Zaimplementuj metodę CChildView::OnSizing . Ta metoda anuluje wszystkie istniejące zadania rysunkowe.

    void CChildView::OnSizing(UINT nSide, LPRECT lpRect)
    {
       // The window size is changing; cancel any existing drawing tasks.
       m_DrawingTasks.cancel();
    }
    
  4. Zaimplementuj metodę CChildView::OnSize . Ta metoda anuluje wszystkie istniejące zadania rysunkowe i tworzy nowe zadanie rysunku dla zaktualizowanego rozmiaru okna klienta.

    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. Zaimplementuj metodę CChildView::OnDestroy . Ta metoda anuluje wszystkie istniejące zadania rysunkowe.

    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. W ChildView.cpp zdefiniuj klasę scope_guard , która implementuje wzorzec 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&);
    };
    
  7. Dodaj następujący kod do CChildView::DrawMandelbrot metody po wywołaniu metody :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);      
    });
    

    Ten kod obsługuje anulowanie przez utworzenie scope_guard obiektu. Gdy obiekt opuszcza zakres, odblokuje bity mapy bitowej.

  8. Zmodyfikuj koniec CChildView::DrawMandelbrot metody , aby odrzucić scope_guard obiekt po odblokowaniu bitów mapy bitowej, ale przed wysłaniem komunikatów do wątku interfejsu użytkownika. Dzięki temu wątek interfejsu użytkownika nie zostanie zaktualizowany przed odblokowaniem bitów mapy bitowej.

    // 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. Sprawdź, czy aplikacja została pomyślnie zaktualizowana, kompilując i uruchamiając ją.

Podczas zmieniania rozmiaru okna praca rysunkowa jest wykonywana tylko dla końcowego rozmiaru okna. Wszystkie aktywne zadania rysunkowe są również anulowane, gdy okno zostanie zniszczone.

[Top]

Zobacz też

Środowisko uruchomieniowe współbieżności — wskazówki
Równoległość zadań
Bloki komunikatów asynchronicznych
Funkcje przekazywania komunikatów
Algorytmy równoległe
Anulowanie w PPL
Aplikacje klasyczne MFC