Návod: Odstranění práce z vlákna uživatelského rozhraní
Tento dokument ukazuje, jak pomocí modulu Concurrency Runtime přesunout práci prováděnou vláknem uživatelského rozhraní (UI) v aplikaci Microsoft Foundation Classes (MFC) do pracovního vlákna. Tento dokument také ukazuje, jak zlepšit výkon zdlouhavé operace kreslení.
Odebráním práce z vlákna uživatelského rozhraní přesměrováním blokujících operací, například kreslení, do pracovních vláken může zlepšit rychlost odezvy aplikace. Tento návod používá rutinu kreslení, která generuje fraktal Mandelbrot k předvedení zdlouhavé blokující operace. Generace fraktalu Mandelbrot je také vhodným kandidátem pro paralelizaci, protože výpočet každého pixelu je nezávislý na všech ostatních výpočtech.
Požadavky
Než začnete s tímto názorem, přečtěte si následující témata:
Před zahájením tohoto návodu také doporučujeme porozumět základům vývoje aplikací MFC a rozhraní GDI+ . Další informace o knihovně MFC naleznete v tématu Desktopové aplikace MFC. Další informace o GDI+, viz GDI+.
Oddíly
Tento názorný postup obsahuje následující části:
Vytvoření aplikace MFC
Tato část popisuje, jak vytvořit základní aplikaci MFC.
Vytvoření aplikace MFC v jazyce Visual C++
Pomocí Průvodce aplikací MFC vytvořte aplikaci MFC se všemi výchozími nastaveními. Viz Návod: Použití nových ovládacích prvků prostředí MFC pro pokyny k otevření průvodce pro vaši verzi sady Visual Studio.
Zadejte název projektu, například
Mandelbrot
a klepněte na tlačítko OK zobrazte Průvodce aplikací MFC.V podokně Typ aplikace vyberte Jeden dokument. Ujistěte se, že není zaškrtnuté políčko Podpora architektury dokumentů a zobrazení.
Kliknutím na tlačítko Dokončit vytvoříte projekt a zavřete Průvodce aplikací MFC.
Ověřte, že se aplikace úspěšně vytvořila, a to sestavením a spuštěním aplikace. Aplikaci sestavíte tak, že v nabídce Sestavení kliknete na Sestavit řešení. Pokud se aplikace úspěšně sestaví, spusťte aplikaci kliknutím na Spustit ladění v nabídce Ladění .
Implementace sériové verze aplikace Mandelbrot
Tato část popisuje, jak nakreslit mandelbrotský fraktal. Tato verze nakreslí Fractal Mandelbrot do objektu GDI+ Bitmap a pak zkopíruje obsah tohoto rastrového obrázku do okna klienta.
Implementace sériové verze aplikace Mandelbrot
Do souboru pch.h (stdafx.h v sadě Visual Studio 2017 a starší) přidejte následující
#include
direktivu:#include <memory>
V ChildView.h, za direktivou
pragma
BitmapPtr
definujte typ. TypBitmapPtr
umožňuje ukazatel naBitmap
objekt, který má být sdílen více komponentami. ObjektBitmap
se odstraní, když už na něj nebude odkazovat žádná komponenta.typedef std::shared_ptr<Gdiplus::Bitmap> BitmapPtr;
V ChildView.h přidejte následující kód do
protected
oddíluCChildView
třídy:protected: // Draws the Mandelbrot fractal to the specified Bitmap object. void DrawMandelbrot(BitmapPtr); protected: ULONG_PTR m_gdiplusToken;
V ChildView.cpp okomentujte nebo odeberte následující řádky.
//#ifdef _DEBUG //#define new DEBUG_NEW //#endif
V buildech Debug zabráníte aplikaci používat
DEBUG_NEW
alokátor, který není kompatibilní s GDI+.V ChildView.cpp přidejte
using
do oboru názvů direktivuGdiplus
.using namespace Gdiplus;
Do konstruktoru a destruktoru
CChildView
třídy přidejte následující kód, který inicializuje a vypne GDI+.CChildView::CChildView() { // Initialize GDI+. GdiplusStartupInput gdiplusStartupInput; GdiplusStartup(&m_gdiplusToken, &gdiplusStartupInput, NULL); } CChildView::~CChildView() { // Shutdown GDI+. GdiplusShutdown(m_gdiplusToken); }
Implementujte metodu
CChildView::DrawMandelbrot
. Tato metoda nakreslí Mandelbrot fraktal na zadaný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); }
Implementujte metodu
CChildView::OnPaint
. Tato metoda voláCChildView::DrawMandelbrot
a potom zkopíruje obsahBitmap
objektu 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); } }
Ověřte, že aplikace byla úspěšně aktualizována sestavením a spuštěním aplikace.
Následující obrázek ukazuje výsledky aplikace Mandelbrot.
Vzhledem k tomu, že výpočet pro každý pixel je výpočetně nákladný, vlákno uživatelského rozhraní nemůže zpracovat další zprávy, dokud se celkový výpočet nedokončí. To může snížit rychlost odezvy v aplikaci. Tento problém však můžete zmírnit odebráním práce z vlákna uživatelského rozhraní.
[Nahoře]
Odebrání práce z vlákna uživatelského rozhraní
Tato část ukazuje, jak odebrat práci výkresu z vlákna uživatelského rozhraní v aplikaci Mandelbrot. Přesunutím kreslicí práce z vlákna uživatelského rozhraní do pracovního vlákna může vlákno uživatelského rozhraní zpracovávat zprávy, protože pracovní vlákno generuje obrázek na pozadí.
Modul Concurrency Runtime nabízí tři způsoby spouštění úloh: skupiny úloh, asynchronní agenty a zjednodušené úlohy. I když můžete k odebrání práce z vlákna uživatelského rozhraní použít některý z těchto mechanismů, v tomto příkladu se používá souběžnost::task_group objekt, protože skupiny úloh podporují zrušení. Tento názorný postup později používá zrušení ke snížení množství práce, která se provádí při změně velikosti okna klienta, a k provedení vyčištění při zničení okna.
Tento příklad také používá souběžnost::unbounded_buffer objekt k povolení vzájemné komunikace vlákna uživatelského rozhraní a pracovního vlákna. Jakmile pracovní vlákno vytvoří obrázek, odešle ukazatel na Bitmap
objekt unbounded_buffer
objektu a pak odešle zprávu malování do vlákna uživatelského rozhraní. Vlákno uživatelského rozhraní pak přijme z objektu unbounded_buffer
Bitmap
objekt a nakreslí ho do okna klienta.
Odebrání práce výkresu z vlákna uživatelského rozhraní
V souboru pch.h (stdafx.h v sadě Visual Studio 2017 a starších) přidejte následující
#include
direktivy:#include <agents.h> #include <ppl.h>
V ChildView.h přidejte
task_group
aunbounded_buffer
členské proměnné doprotected
oddíluCChildView
třídy. Objekt obsahuje úkoly, které provádějí kreslení.unbounded_buffer
Objekttask_group
obsahuje dokončený obrázek Mandelbrotu.concurrency::task_group m_DrawingTasks; concurrency::unbounded_buffer<BitmapPtr> m_MandelbrotImages;
V ChildView.cpp přidejte
using
do oboru názvů direktivuconcurrency
.using namespace concurrency;
CChildView::DrawMandelbrot
V metodě, po voláníBitmap::UnlockBits
volání , volání concurrency::send funkce předatBitmap
objekt do vlákna uživatelského rozhraní. Pak publikujte zprávu malování do vlákna uživatelského rozhraní a zneplatníte oblast 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);
Aktualizujte metodu
CChildView::OnPaint
pro příjem aktualizovanéhoBitmap
objektu a nakreslete obrázek 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
vytvoří úlohu pro vygenerování image Mandelbrot, pokud v vyrovnávací paměti zprávy neexistuje. Vyrovnávací paměť zprávy nebude obsahovatBitmap
objekt v případech, jako je počáteční zpráva malování a při přesunutí jiného okna před okno klienta.Ověřte, že aplikace byla úspěšně aktualizována sestavením a spuštěním aplikace.
Uživatelské rozhraní je teď responzivní, protože práce výkresu se provádí na pozadí.
[Nahoře]
Zlepšení výkonu výkresu
Generace fraktalu Mandelbrot je vhodným kandidátem pro paralelizaci, protože výpočet každého pixelu je nezávislý na všech ostatních výpočtech. Chcete-li paralelizovat proceduru výkresu, převeďte vnější for
smyčku v CChildView::DrawMandelbrot
metodě na volání souběžnosti::p arallel_for algoritmus následujícím způsobem.
// Compute whether each point lies in the Mandelbrot set.
parallel_for (0u, height, [&](UINT row)
{
// Loop body omitted for brevity.
});
Vzhledem k tomu, že výpočet každého rastrového prvku je nezávislý, nemusíte synchronizovat operace kreslení, které přistupují k paměti bitmapy. To umožňuje škálování výkonu při nárůstu počtu dostupných procesorů.
[Nahoře]
Přidání podpory pro zrušení
Tato část popisuje, jak zpracovat změnu velikosti okna a jak zrušit všechny aktivní úkoly výkresu při zničení okna.
Dokument Zrušení v PPL vysvětluje, jak funguje zrušení v modulu runtime. Zrušení je spolupráce; proto nedojde okamžitě. Pokud chcete zastavit zrušenou úlohu, modul runtime vyvolá během následného volání úlohy do modulu runtime vnitřní výjimku. Předchozí část ukazuje, jak použít parallel_for
algoritmus ke zlepšení výkonu úlohy výkresu. Volání, které parallel_for
umožní modulu runtime zastavit úlohu, a proto umožňuje zrušení fungovat.
Rušení aktivních úkolů
Aplikace Mandelbrot vytvoří Bitmap
objekty, jejichž dimenze odpovídají velikosti okna klienta. Pokaždé, když se změní velikost okna klienta, aplikace vytvoří další úlohu na pozadí, která vygeneruje obrázek pro novou velikost okna. Aplikace nevyžaduje tyto zprostředkující image; vyžaduje pouze obrázek pro konečnou velikost okna. Chcete-li zabránit aplikaci v provádění této další práce, můžete zrušit všechny aktivní úkoly kreslení v obslužných rutinách zpráv pro zprávy WM_SIZE
a WM_SIZING
potom přeplánovat práci výkresu po změně velikosti okna.
Chcete-li zrušit aktivní úkoly kreslení při změně velikosti okna, aplikace volá souběžnost::task_group::cancel metoda v obslužných rutinách pro zprávyWM_SIZING
.WM_SIZE
Obslužná rutina zprávy WM_SIZE
také volá souběžnost::task_group::wait metoda čekání na dokončení všech aktivních úkolů a následné přeplánování úkolu výkresu pro aktualizovanou velikost okna.
Když je okno klienta zničeno, je vhodné zrušit všechny aktivní úkoly výkresu. Zrušení všech aktivních úloh výkresu zajistí, že pracovní vlákna nebudou po zničení okna klienta publikovat zprávy do vlákna uživatelského rozhraní. Aplikace zruší všechny aktivní úkoly výkresu v obslužné rutině WM_DESTROY
zprávy.
Reakce na zrušení
Metoda CChildView::DrawMandelbrot
, která provádí úkol výkresu, musí reagovat na zrušení. Vzhledem k tomu, že modul runtime používá zpracování výjimek ke zrušení úloh, CChildView::DrawMandelbrot
musí metoda použít mechanismus bezpečný pro výjimky, který zaručuje, že všechny prostředky jsou správně vyčištěny. V tomto příkladu se používá model Inicializace prostředků (RAII), který zaručuje, že rastrové bity jsou odemknuté při zrušení úkolu.
Přidání podpory pro zrušení v aplikaci Mandelbrot
V ChildView.h, v
protected
částiCChildView
třídy přidejte deklarace proOnSize
,OnSizing
aOnDestroy
funkce mapování zpráv.afx_msg void OnPaint(); afx_msg void OnSize(UINT, int, int); afx_msg void OnSizing(UINT, LPRECT); afx_msg void OnDestroy(); DECLARE_MESSAGE_MAP()
V ChildView.cpp upravte mapu zpráv tak, aby obsahovala obslužné rutiny pro objekty
WM_SIZE
,WM_SIZING
aWM_DESTROY
zprávy.BEGIN_MESSAGE_MAP(CChildView, CWnd) ON_WM_PAINT() ON_WM_SIZE() ON_WM_SIZING() ON_WM_DESTROY() END_MESSAGE_MAP()
Implementujte metodu
CChildView::OnSizing
. Tato metoda zruší všechny existující úkoly výkresu.void CChildView::OnSizing(UINT nSide, LPRECT lpRect) { // The window size is changing; cancel any existing drawing tasks. m_DrawingTasks.cancel(); }
Implementujte metodu
CChildView::OnSize
. Tato metoda zruší všechny existující úkoly výkresu a vytvoří novou úlohu výkresu pro aktualizovanou velikost 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))); }); } }
Implementujte metodu
CChildView::OnDestroy
. Tato metoda zruší všechny existující úkoly výkresu.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(); }
V ChildView.cpp definujte
scope_guard
třídu, která implementuje vzor 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&); };
Po volání
Bitmap::LockBits
přidejte doCChildView::DrawMandelbrot
metody následující kód:// 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); });
Tento kód zpracovává zrušení vytvořením objektu
scope_guard
. Když objekt opustí obor, odemkne rastrové bity.Upravte konec
CChildView::DrawMandelbrot
metody tak, aby zavřelscope_guard
objekt po odemčení rastrových bitů, ale před odesláním všech zpráv do vlákna uživatelského rozhraní. Tím se zajistí, že se vlákno uživatelského rozhraní neaktualizuje před odemknutím bitů rastrového obrázku.// 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);
Ověřte, že aplikace byla úspěšně aktualizována sestavením a spuštěním aplikace.
Při změně velikosti okna se práce výkresu provádí pouze pro konečnou velikost okna. Při zničení okna se zruší také všechny aktivní úkoly kreslení.
[Nahoře]
Viz také
Návody pro Concurrency Runtime
Paralelismus úkolu
Asynchronní bloky zpráv
Funkce pro předávání zpráv
Paralelní algoritmy
Zrušení v knihovně PPL
Desktopové aplikace knihovny MFC