Procedura dettagliata: rimozione di lavoro da un thread dell'interfaccia utente

In questo documento viene illustrato come utilizzare il Runtime di concorrenza per trasferire il lavoro eseguito dal thread dell'interfaccia utente in un'applicazione MFC (Microsoft Foundation Classes) a un thread di lavoro. Viene inoltre illustrato come migliorare le prestazioni di un'operazione di disegno di lunga durata.

La rimozione di lavoro dal thread dell'interfaccia utente trasferendo le operazioni più onerose, ad esempio, il disegno, ai thread di lavoro può migliorare la reattività dell'applicazione. Al fine di presentare un'operazione lunga e onerosa, questa procedura dettagliata utilizza una routine di disegno che genera il frattale di Mandelbrot. La generazione del frattale di Mandelbrot è inoltre un buon candidato per la parallelizzazione perché il calcolo di ciascun pixel è indipendente da tutti gli altri calcoli.

Prerequisiti

Prima di iniziare questa procedura dettagliata, leggere gli argomenti seguenti:

Si consiglia inoltre di acquisire una buona conoscenza delle basi per lo sviluppo di applicazioni MFC e GDI+ prima di avviare questa procedura dettagliata. Per ulteriori informazioni su MFC, vedere MFC Reference. Per ulteriori informazioni su GDI+, vedere GDI+.

Sezioni

In questa procedura dettagliata sono contenute le sezioni seguenti:

  • Creazione dell'applicazione MFC

  • Implementazione della versione seriale dell'applicazione di Mandelbrot

  • Rimozione del lavoro dal thread dell'interfaccia utente

  • Miglioramento delle prestazioni di disegno

  • Aggiunta di supporto per l'annullamento

Creazione dell'applicazione MFC

In questa sezione viene descritto come creare l'applicazione MFC di base.

Per creare un'applicazione MFC in Visual C++

  1. Nel menu File, scegliere Nuovo, quindi fare clic su Progetto.

  2. Nel riquadro Modelli installati della finestra di dialogo Nuovo Progetto, selezionare Visual C++, quindi selezionare Applicazione MFC nel riquadro Modelli. Digitare un nome per il progetto, ad esempio, Mandelbrot, 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 per l'architettura documento/visualizzazione non sia selezionata.

  4. Fare clic su Fine per creare il progetto e chiudere la Creazione guidata applicazione MFC.

    Verificare che l'applicazione sia stata creata correttamente compilandola ed eseguendola. Per compilare l'applicazione, fare clic su Compila soluzione nel menu Compila. Se l'applicazione viene compilata correttamente, eseguirla facendo clic su Avvia debug nel menu Debug.

Implementazione della versione seriale dell'applicazione di Mandelbrot

In questa sezione viene descritto come disegnare il frattale di Mandelbrot. In questa versione il frattale di Mandelbrot viene disegnato in un oggetto GDI+ Bitmap, quindi il contenuto di tale bitmap viene copiato nella finestra client.

Per implementare la versione seriale dell'applicazione di Mandelbrot

  1. In stdafx.h aggiungere la seguente direttiva #include:

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

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

    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
    

    Nelle compilazioni Debug, questo passaggio evita che l'applicazione utilizzi l'allocatore DEBUG_NEW che è incompatibile con GDI+.

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

    using namespace Gdiplus;
    
  6. Aggiungere il codice riportato di seguito al costruttore e al distruttore della classe CChildView per inizializzare e arrestare GDI+.

    CChildView::CChildView()
    {
       // Initialize GDI+.
       GdiplusStartupInput gdiplusStartupInput;
       GdiplusStartup(&m_gdiplusToken, &gdiplusStartupInput, NULL);
    }
    
    CChildView::~CChildView()
    {
       // Shutdown GDI+.
       GdiplusShutdown(m_gdiplusToken);
    }
    
  7. Implementare il metodo CChildView::DrawMandelbrot. Questo metodo disegna il frattale di Mandelbrot nell'oggetto Bitmap specificato.

    // 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. Implementare il metodo CChildView::OnPaint. Questo metodo chiama CChildView::DrawMandelbrot e 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 compilandola ed eseguendola.

Nell'illustrazione riportata di seguito vengono mostrati i risultati dell'applicazione di Mandelbrot.

Applicazione di Mandelbrot

Poiché il calcolo per ciascun pixel è oneroso in termini di elaborazione, il thread dell'interfaccia utente non sarà in grado di elaborare messaggi aggiuntivi fino al termine del calcolo complessivo. Questo potrebbe ridurre la reattività dell'applicazione. Tuttavia, è possibile limitare l'entità di questo problema rimuovendo il lavoro dal thread dell'interfaccia utente.

[vai all'inizio]

Rimozione del lavoro dal thread dell'interfaccia utente

In questa sezione viene illustrato come rimuovere il lavoro di disegno dal thread UI nell'applicazione di Mandelbrot. Trasferendo il lavoro di disegno dal thread UI in un thread di lavoro, il thread UI sarà in grado di elaborare messaggi mentre il thread di lavoro genera in background l'immagine.

Il runtime di concorrenza fornisce tre modalità di esecuzione di attività: gruppi di attività, agenti asincroni e attività leggere. Sebbene sia possibile utilizzare uno qualsiasi di questi meccanismi per rimuovere lavoro dal thread UI, in questo esempio viene utilizzato un oggetto Concurrency::task_group in quanto i gruppi di attività supportano l'annullamento. In questa procedura dettagliata, più avanti verrà utilizzato l'annullamento per ridurre la quantità di lavoro eseguita quando la finestra client viene ridimensionata e per eseguire la pulizia quando la finestra viene distrutta.

In questo esempio viene inoltre utilizzato un oggetto Concurrency::unbounded_buffer per consentire la comunicazione tra il thread UI e il thread di lavoro. Quando il thread di lavoro ha terminato la generazione dell'immagine, invia all'oggetto unbounded_buffer un puntatore all'oggetto Bitmap, quindi invia un messaggio di disegno al thread UI. Il thread UI riceve quindi l'oggetto Bitmap dall'oggetto unbounded_buffer e lo disegna nella finestra client.

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

  1. In stdafx.h aggiungere, le seguenti direttive #include:

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

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

    using namespace Concurrency;
    
  4. Nel metodo CChildView::DrawMandelbrot, dopo la chiamata a Bitmap::UnlockBits, chiamare la funzione Concurrency::send per passare l'oggetto Bitmap al thread UI. Inviare quindi un messaggio di disegno al thread UI 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 metodo CChildView::OnPaint per ricevere l'oggetto Bitmap aggiornato 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 metodo CChildView::OnPaint crea un'attività per generare l'immagine di Mandelbrot, se non ne esiste una nel buffer dei messaggi. Il buffer dei messaggi non conterrà un oggetto Bitmap in casi come il messaggio di disegno iniziale e quando un'altra finestra viene spostata davanti alla finestra client.

  6. Verificare che l'applicazione sia stata aggiornata correttamente compilandola ed eseguendola.

L'Interfaccia utente è ora più reattiva perché il lavoro di disegno viene eseguito in background.

[vai all'inizio]

Miglioramento delle prestazioni di disegno

La generazione del frattale di Mandelbrot è un buon candidato per la parallelizzazione perché il calcolo di ciascun pixel è indipendente da tutti gli altri calcoli. Per parallelizzare la procedura di disegno, convertire il ciclo for esterno del metodo CChildView::DrawMandelbrot in una chiamata all'algoritmo Concurrency::parallel_for, come illustrato 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 ciascun elemento della bitmap è indipendente, non è necessario sincronizzare le operazioni di disegno che accedono alla memoria della bitmap. Questo consente l'adeguamento delle prestazioni all'aumentare del numero di processori disponibili.

[vai all'inizio]

Aggiunta di supporto per l'annullamento

In questa sezione viene descritto come gestire il ridimensionamento della finestra e come annullare qualsiasi attività di disegno attiva quando viene distrutta la finestra.

Il documento Annullamento nella libreria PPL spiega il funzionamento dell'annullamento nel runtime. L'annullamento è un processo di cooperazione, pertanto non si verifica immediatamente. Per arrestare un'attività annullata, il runtime genera un'eccezione interna durante una chiamata successiva inviata dall'attività nel 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 pertanto consente il funzionamento dell'annullamento.

Annullamento di attività attive

L'applicazione di Mandelbrot crea oggetti Bitmap 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 la nuova dimensione della finestra. L'applicazione non necessita di tali immagini intermedie; richiede solo l'immagine corrispondente alla dimensione finale della finestra. Per evitare l'esecuzione di questo lavoro aggiuntivo da parte dell'applicazione, è possibile annullare qualsiasi attività di disegno attiva nei gestori di messaggi relativamente ai messaggi WM_SIZE e WM_SIZING e quindi ripianificare il lavoro di disegno dopo che la finestra è stata ridimensionata.

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 messaggi WM_SIZING e WM_SIZE. Il gestore del messaggio WM_SIZE chiama inoltre il metodo Concurrency::task_group::wait per attendere il completamento di tutte le attività attive e quindi ripianificare l'attività di disegno per la dimensione della finestra aggiornata.

Quando la finestra client viene distrutta, è consigliabile annullare qualsiasi attività di disegno attiva. L'annullamento di qualsiasi attività di disegno attiva garantisce che i thread di lavoro non inviino messaggi al thread UI dopo che la finestra client è stata distrutta. L'applicazione annulla qualsiasi attività di disegno attiva nel gestore per il messaggio WM_DESTROY.

Risposta all'annullamento

Il metodo CChildView::DrawMandelbrot che esegue l'attività di disegno deve rispondere all'annullamento. Poiché il runtime utilizza la gestione delle eccezioni per annullare le attività, il metodo CChildView::DrawMandelbrot deve utilizzare un meccanismo indipendente dalle eccezioni per garantire la corretta pulizia di tutte le risorse. In questo esempio viene utilizzato il modello RAII (Resource Acquisition Is Initialization) per garantire che i bit della bitmap vengano sbloccati quando l'attività viene annullata.

Per aggiungere supporto per l'annullamento nell'applicazione di Mandelbrot

  1. In ChildView.h, nella sezione protected della classe CChildView, aggiungere le dichiarazioni per le funzioni OnSize, OnSizing e OnDestroy di mapping ai messaggi.

    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 per contenere i gestori per i messaggi WM_SIZE, WM_SIZING e WM_DESTROY.

    BEGIN_MESSAGE_MAP(CChildView, CWnd)
       ON_WM_PAINT()
       ON_WM_SIZE()
       ON_WM_SIZING()
       ON_WM_DESTROY()
    END_MESSAGE_MAP()
    
  3. Implementare il metodo CChildView::OnSizing. Questo metodo annulla qualsiasi attività di disegno esistente.

    void CChildView::OnSizing(UINT nSide, LPRECT lpRect)
    {
       // The window size is changing; cancel any existing drawing tasks.
       m_DrawingTasks.cancel();
    }
    
  4. Implementare il metodo CChildView::OnSize. Questo metodo annulla qualsiasi attività di disegno esistente e crea una nuova attività di disegno per la dimensione della finestra client aggiornata.

    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. Implementare il metodo CChildView::OnDestroy. Questo metodo annulla qualsiasi attività di disegno esistente.

    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 classe scope_guard 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 riportato di seguito al metodo CChildView::DrawMandelbrot 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);      
    });
    

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

  8. Modificare la fine del metodo CChildView::DrawMandelbrot in modo da terminare l'oggetto scope_guard dopo aver sbloccato bit della bitmap, ma prima dell'invio di qualsiasi messaggio al thread UI. Questo assicura che il thread UI non venga aggiornato prima di aver sbloccato i bit della bitmap.

    // 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 compilandola ed eseguendola.

Quando si ridimensiona la finestra, il lavoro di disegno viene eseguito solo per la dimensione della finestra finale. Qualsiasi attività di disegno attiva viene annullata anche quando la finestra viene distrutta.

[vai all'inizio]

Vedere anche

Concetti

Procedure dettagliate del runtime di concorrenza

Parallelismo delle attività (runtime di concorrenza)

Blocchi dei messaggi asincroni

Funzioni di passaggio dei messaggi

Algoritmi paralleli

Annullamento nella libreria PPL

Altre risorse

MFC Reference