Procedura dettagliata: rimozione di lavoro da un thread dell'interfaccia utente
Questo documento illustra come usare il runtime di concorrenza per spostare il lavoro eseguito dal thread dell'interfaccia utente in un'applicazione MFC (Microsoft Foundation Classes) in un thread di lavoro. Questo documento illustra anche come migliorare le prestazioni di un'operazione di disegno lunga.
La rimozione del lavoro dal thread dell'interfaccia utente tramite l'offload delle operazioni di blocco, ad esempio il disegno, ai thread di lavoro può migliorare la velocità di risposta dell'applicazione. In questa procedura dettagliata viene utilizzata una routine di disegno che genera il fractal di Mandelbrot per dimostrare un'operazione di blocco lunga. La generazione del fractal di Mandelbrot è anche un buon candidato per la parallelizzazione perché il calcolo di ogni pixel è indipendente da tutti gli altri calcoli.
Prerequisiti
Leggere gli argomenti seguenti prima di iniziare questa procedura dettagliata:
È anche consigliabile comprendere le nozioni di base dello sviluppo di applicazioni MFC e GDI+ prima di iniziare questa procedura dettagliata. Per altre informazioni su MFC, vedere Applicazioni desktop MFC. Per altre informazioni su GDI+, vedere GDI+.
Sezioni
Questa procedura dettagliata contiene le sezioni seguenti:
Creazione dell'applicazione MFC
Questa sezione descrive come creare l'applicazione MFC di base.
Per creare un'applicazione MFC visual C++
Usare la Creazione guidata applicazione MFC per creare un'applicazione MFC con tutte le impostazioni predefinite. Vedere Procedura dettagliata: Uso dei nuovi controlli shell MFC per istruzioni su come aprire la procedura guidata per la versione di Visual Studio.
Digitare un nome per il progetto,
Mandelbrot
ad esempio , e quindi fare clic su OK per visualizzare la Creazione guidata applicazione MFC.Nel riquadro Tipo di applicazione selezionare Documento singolo. Verificare che la casella di controllo Supporto architettura documento/visualizzazione sia deselezionata.
Fare clic su Fine per creare il progetto e chiudere la Creazione guidata applicazione MFC.
Verificare che l'applicazione sia stata creata correttamente compilando ed eseguendola. Per compilare l'applicazione, scegliere Compila soluzione dal menu Compila. Se l'applicazione viene compilata correttamente, eseguire l'applicazione facendo clic su Avvia debug dal menu Debug .
Implementazione della versione seriale dell'applicazione Mandelbrot
Questa sezione descrive come disegnare il fractal di Mandelbrot. Questa versione disegna il fractal di Mandelbrot in un oggetto GDI+ Bitmap e quindi copia il contenuto di tale bitmap nella finestra client.
Per implementare la versione seriale dell'applicazione Mandelbrot
In pch.h (stdafx.h in Visual Studio 2017 e versioni precedenti), aggiungere la direttiva seguente
#include
:#include <memory>
In ChildView.h, dopo la
pragma
direttiva, definire ilBitmapPtr
tipo. IlBitmapPtr
tipo consente a un puntatore a unBitmap
oggetto di essere condiviso da più componenti. L'oggettoBitmap
viene eliminato quando non viene più fatto riferimento da alcun componente.typedef std::shared_ptr<Gdiplus::Bitmap> BitmapPtr;
In ChildView.h aggiungere il codice seguente alla
protected
sezione dellaCChildView
classe :protected: // Draws the Mandelbrot fractal to the specified Bitmap object. void DrawMandelbrot(BitmapPtr); protected: ULONG_PTR m_gdiplusToken;
In ChildView.cpp impostare come commento o rimuovere le righe seguenti.
//#ifdef _DEBUG //#define new DEBUG_NEW //#endif
In Compilazioni di debug questo passaggio impedisce all'applicazione di usare l'allocatore
DEBUG_NEW
, che non è compatibile con GDI+.In ChildView.cpp aggiungere una
using
direttiva allo spazio deiGdiplus
nomi .using namespace Gdiplus;
Aggiungere il codice seguente al costruttore e al distruttore della
CChildView
classe per inizializzare e arrestare GDI+.CChildView::CChildView() { // Initialize GDI+. GdiplusStartupInput gdiplusStartupInput; GdiplusStartup(&m_gdiplusToken, &gdiplusStartupInput, NULL); } CChildView::~CChildView() { // Shutdown GDI+. GdiplusShutdown(m_gdiplusToken); }
Implementa il metodo
CChildView::DrawMandelbrot
. Questo metodo disegna il fractal di Mandelbrot all'oggetto specificatoBitmap
.// 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); }
Implementa il metodo
CChildView::OnPaint
. Questo metodo chiamaCChildView::DrawMandelbrot
e quindi copia il contenuto dell'oggettoBitmap
nella finestra.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); } }
Verificare che l'applicazione sia stata aggiornata correttamente compilando ed eseguendola.
La figura seguente mostra i risultati dell'applicazione Mandelbrot.
Poiché il calcolo per ogni pixel è costoso dal livello di calcolo, il thread dell'interfaccia utente non può elaborare messaggi aggiuntivi fino al termine del calcolo complessivo. Ciò potrebbe ridurre la velocità di risposta nell'applicazione. Tuttavia, è possibile alleviare questo problema rimuovendo il lavoro dal thread dell'interfaccia utente.
Rimozione del lavoro dal thread dell'interfaccia utente
Questa sezione illustra come rimuovere il lavoro di disegno dal thread dell'interfaccia utente nell'applicazione Mandelbrot. Spostando il lavoro di disegno dal thread dell'interfaccia utente a un thread di lavoro, il thread dell'interfaccia utente può elaborare i messaggi mentre il thread di lavoro genera l'immagine in background.
Il runtime di concorrenza offre tre modi per eseguire attività: gruppi di attività, agenti asincroni e attività leggere. Sebbene sia possibile usare uno di questi meccanismi per rimuovere il lavoro dal thread dell'interfaccia utente, questo esempio usa un oggetto concurrency::task_group perché i gruppi di attività supportano l'annullamento. Questa procedura dettagliata usa successivamente l'annullamento per ridurre la quantità di lavoro eseguita quando la finestra client viene ridimensionata e per eseguire la pulizia quando la finestra viene eliminata definitivamente.
Questo esempio usa anche un oggetto concurrency::unbounded_buffer per consentire al thread dell'interfaccia utente e al thread di lavoro di comunicare tra loro. Dopo che il thread di lavoro produce l'immagine, invia un puntatore all'oggetto all'oggetto Bitmap
unbounded_buffer
e quindi invia un messaggio di disegno al thread dell'interfaccia utente. Il thread dell'interfaccia utente riceve quindi dall'oggetto l'oggetto unbounded_buffer
Bitmap
e lo disegna nella finestra client.
Per rimuovere il lavoro di disegno dal thread dell'interfaccia utente
In pch.h (stdafx.h in Visual Studio 2017 e versioni precedenti), aggiungere le direttive seguenti
#include
:#include <agents.h> #include <ppl.h>
In ChildView.h aggiungere
task_group
le variabili membro eunbounded_buffer
allaprotected
sezione dellaCChildView
classe . L'oggettotask_group
contiene le attività che eseguono il disegno; l'oggettounbounded_buffer
contiene l'immagine completa di Mandelbrot.concurrency::task_group m_DrawingTasks; concurrency::unbounded_buffer<BitmapPtr> m_MandelbrotImages;
In ChildView.cpp aggiungere una
using
direttiva allo spazio deiconcurrency
nomi .using namespace concurrency;
CChildView::DrawMandelbrot
Nel metodo, dopo la chiamata aBitmap::UnlockBits
, chiamare la funzione concurrency::send per passare l'oggetto al thread dell'interfacciaBitmap
utente. Inviare quindi un messaggio di disegno al thread dell'interfaccia utente e invalidare l'area client.// 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);
Aggiornare il
CChildView::OnPaint
metodo per ricevere l'oggetto aggiornatoBitmap
e disegnare l'immagine nella finestra client.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))); }); } }
Il
CChildView::OnPaint
metodo crea un'attività per generare l'immagine di Mandelbrot, se non esiste nel buffer dei messaggi. Il buffer dei messaggi non conterrà unBitmap
oggetto nei casi come il messaggio di disegno iniziale e quando viene spostata un'altra finestra davanti alla finestra client.Verificare che l'applicazione sia stata aggiornata correttamente compilando ed eseguendola.
L'interfaccia utente è ora più reattiva perché il lavoro di disegno viene eseguito in background.
Miglioramento delle prestazioni del disegno
La generazione del fractal di Mandelbrot è un buon candidato per la parallelizzazione perché il calcolo di ogni pixel è indipendente da tutti gli altri calcoli. Per parallelizzare la routine di disegno, convertire il ciclo esterno for
nel CChildView::DrawMandelbrot
metodo in una chiamata all'algoritmo concurrency::p arallel_for , come indicato di seguito.
// Compute whether each point lies in the Mandelbrot set.
parallel_for (0u, height, [&](UINT row)
{
// Loop body omitted for brevity.
});
Poiché il calcolo di ogni elemento bitmap è indipendente, non è necessario sincronizzare le operazioni di disegno che accedono alla memoria bitmap. Ciò consente di ridimensionare le prestazioni man mano che aumenta il numero di processori disponibili.
Aggiunta del supporto per l'annullamento
Questa sezione descrive come gestire il ridimensionamento delle finestre e come annullare le attività di disegno attive quando la finestra viene eliminata definitivamente.
Il documento Annullamento nel PPL illustra il funzionamento dell'annullamento nel runtime. L'annullamento è cooperativo; pertanto, non si verifica immediatamente. Per arrestare un'attività annullata, il runtime genera un'eccezione interna durante una chiamata successiva dall'attività al runtime. Nella sezione precedente viene illustrato come utilizzare l'algoritmo parallel_for
per migliorare le prestazioni dell'attività di disegno. La chiamata a parallel_for
consente al runtime di arrestare l'attività e di conseguenza consente il funzionamento dell'annullamento.
Annullamento di attività attive
L'applicazione Mandelbrot crea Bitmap
oggetti le cui dimensioni corrispondono alle dimensioni della finestra client. Ogni volta che la finestra client viene ridimensionata, l'applicazione crea un'attività in background aggiuntiva per generare un'immagine per le nuove dimensioni della finestra. L'applicazione non richiede queste immagini intermedie; richiede solo l'immagine per le dimensioni finali della finestra. Per impedire all'applicazione di eseguire questo lavoro aggiuntivo, è possibile annullare tutte le attività di disegno attive nei gestori di messaggi per i WM_SIZE
messaggi e WM_SIZING
e quindi riprogrammare il lavoro di disegno dopo il ridimensionamento della finestra.
Per annullare le attività di disegno attive quando la finestra viene ridimensionata, l'applicazione chiama il metodo concurrency::task_group::cancel nei gestori per i WM_SIZING
messaggi e WM_SIZE
. Il gestore per il WM_SIZE
messaggio chiama anche il metodo concurrency::task_group::wait per attendere il completamento di tutte le attività attive e quindi riprogramma l'attività di disegno per le dimensioni aggiornate della finestra.
Quando la finestra client viene eliminata definitivamente, è consigliabile annullare tutte le attività di disegno attive. L'annullamento di tutte le attività di disegno attive garantisce che i thread di lavoro non eseseguono messaggi al thread dell'interfaccia utente dopo che la finestra del client viene eliminata definitivamente. L'applicazione annulla tutte le attività di disegno attive nel gestore per il WM_DESTROY
messaggio.
Risposta all'annullamento
Il CChildView::DrawMandelbrot
metodo, che esegue l'attività di disegno, deve rispondere all'annullamento. Poiché il runtime usa la gestione delle eccezioni per annullare le attività, il CChildView::DrawMandelbrot
metodo deve usare un meccanismo indipendente dalle eccezioni per garantire che tutte le risorse siano correttamente pulite. In questo esempio viene usato il modello Di inizializzazione delle risorse (RAII) per garantire che i bit bitmap vengano sbloccati quando l'attività viene annullata.
Per aggiungere il supporto per l'annullamento nell'applicazione Mandelbrot
In ChildView.h, nella
protected
sezione dellaCChildView
classe aggiungere dichiarazioni per leOnSize
funzioni mappa messaggi ,OnSizing
eOnDestroy
.afx_msg void OnPaint(); afx_msg void OnSize(UINT, int, int); afx_msg void OnSizing(UINT, LPRECT); afx_msg void OnDestroy(); DECLARE_MESSAGE_MAP()
In ChildView.cpp modificare la mappa dei messaggi in modo da contenere gestori per i
WM_SIZE
messaggi ,WM_SIZING
eWM_DESTROY
.BEGIN_MESSAGE_MAP(CChildView, CWnd) ON_WM_PAINT() ON_WM_SIZE() ON_WM_SIZING() ON_WM_DESTROY() END_MESSAGE_MAP()
Implementa il metodo
CChildView::OnSizing
. Questo metodo annulla tutte le attività di disegno esistenti.void CChildView::OnSizing(UINT nSide, LPRECT lpRect) { // The window size is changing; cancel any existing drawing tasks. m_DrawingTasks.cancel(); }
Implementa il metodo
CChildView::OnSize
. Questo metodo annulla le attività di disegno esistenti e crea una nuova attività di disegno per le dimensioni aggiornate della finestra client.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))); }); } }
Implementa il metodo
CChildView::OnDestroy
. Questo metodo annulla tutte le attività di disegno esistenti.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(); }
In ChildView.cpp definire la
scope_guard
classe , che implementa il modello 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&); };
Aggiungere il codice seguente al
CChildView::DrawMandelbrot
metodo dopo la chiamata aBitmap::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); });
Questo codice gestisce l'annullamento creando un
scope_guard
oggetto . Quando l'oggetto lascia l'ambito, sblocca i bit bitmap.Modificare la fine del
CChildView::DrawMandelbrot
metodo per ignorare l'oggettoscope_guard
dopo lo sblocco dei bit bitmap, ma prima che tutti i messaggi vengano inviati al thread dell'interfaccia utente. In questo modo si garantisce che il thread dell'interfaccia utente non venga aggiornato prima che i bit bitmap vengano sbloccati.// 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);
Verificare che l'applicazione sia stata aggiornata correttamente compilando ed eseguendola.
Quando si ridimensiona la finestra, il lavoro di disegno viene eseguito solo per le dimensioni finali della finestra. Tutte le attività di disegno attive vengono annullate anche quando la finestra viene eliminata definitivamente.
Vedi anche
Procedure dettagliate del runtime di concorrenza
Parallelismo delle attività
Blocchi dei messaggi asincroni
Funzioni di passaggio dei messaggi
Algoritmi paralleli
Annullamento nella libreria PPL
Applicazioni desktop MFC