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

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

  2. Zadejte název projektu, například Mandelbrota klepněte na tlačítko OK zobrazte Průvodce aplikací MFC.

  3. V podokně Typ aplikace vyberte Jeden dokument. Ujistěte se, že není zaškrtnuté políčko Podpora architektury dokumentů a zobrazení.

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

  1. Do souboru pch.h (stdafx.h v sadě Visual Studio 2017 a starší) přidejte následující #include direktivu:

    #include <memory>
    
  2. V ChildView.h, za direktivou pragmaBitmapPtr definujte typ. Typ BitmapPtr umožňuje ukazatel na Bitmap objekt, který má být sdílen více komponentami. Objekt Bitmap se odstraní, když už na něj nebude odkazovat žádná komponenta.

    typedef std::shared_ptr<Gdiplus::Bitmap> BitmapPtr;
    
  3. V ChildView.h přidejte následující kód do protected oddílu CChildView třídy:

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

  5. V ChildView.cpp přidejte direktivu usingGdiplus do oboru názvů.

    using namespace Gdiplus;
    
  6. 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);
    }
    
  7. 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);
    }
    
  8. Implementujte metodu CChildView::OnPaint . Tato metoda volá CChildView::DrawMandelbrot a potom zkopíruje obsah Bitmap 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);
       }
    }
    
  9. 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.

The Mandelbrot Application.

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_bufferBitmap objekt a nakreslí ho do okna klienta.

Odebrání práce výkresu z vlákna uživatelského rozhraní

  1. 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>
    
  2. V ChildView.h přidejte task_group a unbounded_buffer členské proměnné do protected oddílu CChildView třídy. Objekt obsahuje úkoly, které provádějí kreslení. unbounded_buffer Objekt task_group obsahuje dokončený obrázek Mandelbrotu.

    concurrency::task_group m_DrawingTasks;
    concurrency::unbounded_buffer<BitmapPtr> m_MandelbrotImages;
    
  3. V ChildView.cpp přidejte direktivu usingconcurrency do oboru názvů.

    using namespace concurrency;
    
  4. CChildView::DrawMandelbrot V metodě, po volání Bitmap::UnlockBitsvolání , volání concurrency::send funkce předat Bitmap 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);
    
  5. Aktualizujte metodu CChildView::OnPaint pro příjem aktualizovaného Bitmap 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 obsahovat Bitmap 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.

  6. 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
  1. V ChildView.h, v protected části CChildView třídy přidejte deklarace pro OnSize, OnSizinga OnDestroy 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()
    
  2. V ChildView.cpp upravte mapu zprávy tak, aby obsahoval obslužné rutiny pro WM_SIZE, WM_SIZINGa WM_DESTROY zprávy.

    BEGIN_MESSAGE_MAP(CChildView, CWnd)
       ON_WM_PAINT()
       ON_WM_SIZE()
       ON_WM_SIZING()
       ON_WM_DESTROY()
    END_MESSAGE_MAP()
    
  3. 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();
    }
    
  4. 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)));
          });
       }
    }
    
  5. 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();
    }
    
  6. 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&);
    };
    
  7. Po volání Bitmap::LockBitspřidejte do CChildView::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.

  8. Upravte konec CChildView::DrawMandelbrot metody tak, aby zavřel scope_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);
    
  9. 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