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++
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.
Wpisz nazwę projektu, na przykład , a następnie kliknij przycisk OK,
Mandelbrot
aby wyświetlić Kreatora aplikacji MFC.W okienku Typ aplikacji wybierz pozycję Pojedynczy dokument. Upewnij się, że pole wyboru Obsługa architektury dokumentu/widoku zostało wyczyszczone.
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
W pliku pch.h (stdafx.h w programie Visual Studio 2017 i starszych wersjach) dodaj następującą
#include
dyrektywę:#include <memory>
W pliku ChildView.h po dyrektywie zdefiniuj
pragma
BitmapPtr
typ. TypBitmapPtr
umożliwia udostępnianie przez wiele składników wskaźnikaBitmap
do obiektu. ObiektBitmap
jest usuwany, gdy nie odwołuje się już do niego żaden składnik.typedef std::shared_ptr<Gdiplus::Bitmap> BitmapPtr;
W pliku ChildView.h dodaj następujący kod do
protected
sekcjiCChildView
klasy:protected: // Draws the Mandelbrot fractal to the specified Bitmap object. void DrawMandelbrot(BitmapPtr); protected: ULONG_PTR m_gdiplusToken;
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+.W ChildView.cpp dodaj dyrektywę
using
doGdiplus
przestrzeni nazw.using namespace Gdiplus;
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); }
Zaimplementuj metodę
CChildView::DrawMandelbrot
. Ta metoda rysuje fraktal Mandelbrot do określonegoBitmap
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); }
Zaimplementuj metodę
CChildView::OnPaint
. Ta metoda wywołujeCChildView::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); } }
Sprawdź, czy aplikacja została pomyślnie zaktualizowana, kompilując i uruchamiając ją.
Poniższa ilustracja przedstawia wyniki aplikacji 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
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>
W pliku ChildView.h dodaj
task_group
zmienne składowe iunbounded_buffer
doprotected
sekcjiCChildView
klasy. Obiekttask_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;
W ChildView.cpp dodaj dyrektywę
using
doconcurrency
przestrzeni nazw.using namespace concurrency;
W metodzie po wywołaniu
CChildView::DrawMandelbrot
metodyBitmap::UnlockBits
wywoł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);
Zaktualizuj metodę w celu odebrania
CChildView::OnPaint
zaktualizowanegoBitmap
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.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
W pliku ChildView.h w
protected
sekcjiCChildView
klasy dodaj deklaracje dla funkcji mapy komunikatówOnSize
i , 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()
W ChildView.cpp zmodyfikuj mapę komunikatów tak, aby zawierała programy obsługi dla komunikatów
WM_SIZE
,WM_SIZING
iWM_DESTROY
.BEGIN_MESSAGE_MAP(CChildView, CWnd) ON_WM_PAINT() ON_WM_SIZE() ON_WM_SIZING() ON_WM_DESTROY() END_MESSAGE_MAP()
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(); }
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))); }); } }
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(); }
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&); };
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.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);
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