Condividi tramite


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++

  1. 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.

  2. Digitare un nome per il progetto, Mandelbrotad esempio , e quindi fare clic su OK per visualizzare la Creazione guidata applicazione MFC.

  3. Nel riquadro Tipo di applicazione selezionare Documento singolo. Verificare che la casella di controllo Supporto architettura documento/visualizzazione sia deselezionata.

  4. 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

  1. In pch.h (stdafx.h in Visual Studio 2017 e versioni precedenti), aggiungere la direttiva seguente #include :

    #include <memory>
    
  2. In ChildView.h, dopo la pragma direttiva, definire il BitmapPtr tipo. Il BitmapPtr tipo consente a un puntatore a un Bitmap oggetto di essere condiviso da più componenti. L'oggetto Bitmap viene eliminato quando non viene più fatto riferimento da alcun componente.

    typedef std::shared_ptr<Gdiplus::Bitmap> BitmapPtr;
    
  3. In ChildView.h aggiungere il codice seguente alla protected sezione della CChildView classe :

    protected:
       // Draws the Mandelbrot fractal to the specified Bitmap object.
       void DrawMandelbrot(BitmapPtr);
    
    protected:
       ULONG_PTR m_gdiplusToken;
    
  4. 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+.

  5. In ChildView.cpp aggiungere una using direttiva allo spazio dei Gdiplus nomi .

    using namespace Gdiplus;
    
  6. 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);
    }
    
  7. Implementa il metodo CChildView::DrawMandelbrot. Questo metodo disegna il fractal di Mandelbrot all'oggetto specificato Bitmap .

    // 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. Implementa il metodo CChildView::OnPaint. Questo metodo chiama CChildView::DrawMandelbrot e quindi copia il contenuto dell'oggetto Bitmap 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);
       }
    }
    
  9. Verificare che l'applicazione sia stata aggiornata correttamente compilando ed eseguendola.

La figura seguente mostra i risultati dell'applicazione Mandelbrot.

The Mandelbrot Application.

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.

[Torna all'inizio]

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 Bitmapunbounded_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_bufferBitmap e lo disegna nella finestra client.

Per rimuovere il lavoro di disegno dal thread dell'interfaccia utente

  1. In pch.h (stdafx.h in Visual Studio 2017 e versioni precedenti), aggiungere le direttive seguenti #include :

    #include <agents.h>
    #include <ppl.h>
    
  2. In ChildView.h aggiungere task_group le variabili membro e unbounded_buffer alla protected sezione della CChildView classe . L'oggetto task_group contiene le attività che eseguono il disegno; l'oggetto unbounded_buffer contiene l'immagine completa di Mandelbrot.

    concurrency::task_group m_DrawingTasks;
    concurrency::unbounded_buffer<BitmapPtr> m_MandelbrotImages;
    
  3. In ChildView.cpp aggiungere una using direttiva allo spazio dei concurrency nomi .

    using namespace concurrency;
    
  4. CChildView::DrawMandelbrot Nel metodo, dopo la chiamata a Bitmap::UnlockBits, chiamare la funzione concurrency::send per passare l'oggetto al thread dell'interfaccia Bitmap 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);
    
  5. Aggiornare il CChildView::OnPaint metodo per ricevere l'oggetto aggiornato Bitmap 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à un Bitmap oggetto nei casi come il messaggio di disegno iniziale e quando viene spostata un'altra finestra davanti alla finestra client.

  6. 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.

[Torna all'inizio]

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.

[Torna all'inizio]

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
  1. In ChildView.h, nella protected sezione della CChildView classe aggiungere dichiarazioni per le OnSizefunzioni mappa messaggi , OnSizinge OnDestroy .

    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. In ChildView.cpp modificare la mappa dei messaggi in modo da contenere gestori per i WM_SIZEmessaggi , WM_SIZINGe WM_DESTROY .

    BEGIN_MESSAGE_MAP(CChildView, CWnd)
       ON_WM_PAINT()
       ON_WM_SIZE()
       ON_WM_SIZING()
       ON_WM_DESTROY()
    END_MESSAGE_MAP()
    
  3. 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();
    }
    
  4. 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)));
          });
       }
    }
    
  5. 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();
    }
    
  6. 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&);
    };
    
  7. Aggiungere il codice seguente al CChildView::DrawMandelbrot metodo dopo la chiamata a 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);      
    });
    

    Questo codice gestisce l'annullamento creando un scope_guard oggetto . Quando l'oggetto lascia l'ambito, sblocca i bit bitmap.

  8. Modificare la fine del CChildView::DrawMandelbrot metodo per ignorare l'oggetto scope_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);
    
  9. 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.

[Torna all'inizio]

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