Share via


연습: 사용자 인터페이스 스레드에서 작업 제거

이 문서에서는 동시성 런타임을 사용하여 MFC(Microsoft Foundation Classes) 애플리케이션의 UI(사용자 인터페이스) 스레드에서 수행하는 작업을 작업자 스레드로 이동하는 방법을 보여 줍니다. 이 문서에서는 긴 그리기 작업의 성능을 향상시키는 방법도 보여 줍니다.

차단 작업(예: 그리기)을 작업자 스레드로 오프로드하여 UI 스레드에서 작업을 제거하면 애플리케이션의 응답성이 향상될 수 있습니다. 이 연습에서는 만델브로트 프랙탈을 생성하는 그리기 루틴을 사용하여 긴 차단 작업을 보여 줍니다. 각 픽셀의 계산은 다른 모든 계산과 독립적이므로 만델브로트 프랙탈의 생성도 병렬화에 적합한 후보입니다.

필수 조건

이 연습을 시작하기 전에 다음 항목을 읽어보세요.

또한 이 연습을 시작하기 전에 MFC 애플리케이션 개발 및 GDI+의 기본 사항을 이해하는 것이 좋습니다. MFC에 대한 자세한 내용은 MFC 데스크톱 애플리케이션을 참조 하세요. GDI+에 대한 자세한 내용은 GDI+를 참조 하세요.

섹션

이 연습에는 다음과 같은 섹션이 있습니다.

MFC 애플리케이션 만들기

이 섹션에서는 기본 MFC 애플리케이션을 만드는 방법을 설명합니다.

Visual C++ MFC 애플리케이션을 만들려면

  1. MFC 애플리케이션 마법사사용하여 모든 기본 설정을 사용하여 MFC 애플리케이션을 만듭니다. 연습: 새 MFC 셸 컨트롤을 사용하여 Visual Studio 버전에 대한 마법사를 여는 방법에 대한 지침을 참조하세요.

  2. 예를 들어 Mandelbrot프로젝트의 이름을 입력한 다음 확인을 클릭하여 MFC 애플리케이션 마법사표시합니다.

  3. 애플리케이션 유형 창에서 단일 문서를 선택합니다. 문서/보기 아키텍처 지원 검사 상자가 선택 취소되어 있는지 확인합니다.

  4. 마침을 클릭하여 프로젝트를 만들고 MFC 애플리케이션 마법사닫습니다.

    애플리케이션을 빌드하고 실행하여 성공적으로 생성되었는지 확인합니다. 애플리케이션을 빌드하려면 빌드 메뉴에서 솔루션 빌드를 클릭합니다. 애플리케이션이 성공적으로 빌드되면 디버그 메뉴에서 디버깅 시작을 클릭하여 애플리케이션을실행합니다.

Mandelbrot 애플리케이션의 직렬 버전 구현

이 섹션에서는 만델브로트 프랙탈을 그리는 방법을 설명합니다. 이 버전은 GDI+ 비트맵 개체에 Mandelbrot 프랙탈을 그린 다음 해당 비트맵의 내용을 클라이언트 창에 복사합니다.

Mandelbrot 애플리케이션의 직렬 버전을 구현하려면

  1. pch.h(Visual Studio 2017 이하의 stdafx.h)에서 다음 #include 지시문을 추가합니다.

    #include <memory>
    
  2. ChildView.h에서 지시문 뒤에 pragma 형식을 정의합니다 BitmapPtr . 이 BitmapPtr 형식을 사용하면 개체에 대한 포인터를 Bitmap 여러 구성 요소에서 공유할 수 있습니다. Bitmap 개체는 더 이상 구성 요소에서 참조되지 않을 때 삭제됩니다.

    typedef std::shared_ptr<Gdiplus::Bitmap> BitmapPtr;
    
  3. ChildView.h에서 클래스의 섹션에 protected 다음 코드를 추가합니다 CChildView .

    protected:
       // Draws the Mandelbrot fractal to the specified Bitmap object.
       void DrawMandelbrot(BitmapPtr);
    
    protected:
       ULONG_PTR m_gdiplusToken;
    
  4. ChildView.cpp에서 다음 줄을 주석으로 처리하거나 제거합니다.

    //#ifdef _DEBUG
    //#define new DEBUG_NEW
    //#endif
    

    디버그 빌드에서 이 단계는 애플리케이션이 GDI+와 호환되지 않는 할당자를 사용하지 DEBUG_NEW 못하도록 합니다.

  5. ChildView.cpp에서 네임스페이 using 스에 지시문을 Gdiplus 추가합니다.

    using namespace Gdiplus;
    
  6. 다음 코드를 클래스의 CChildView 생성자 및 소멸자에 추가하여 GDI+를 초기화하고 종료합니다.

    CChildView::CChildView()
    {
       // Initialize GDI+.
       GdiplusStartupInput gdiplusStartupInput;
       GdiplusStartup(&m_gdiplusToken, &gdiplusStartupInput, NULL);
    }
    
    CChildView::~CChildView()
    {
       // Shutdown GDI+.
       GdiplusShutdown(m_gdiplusToken);
    }
    
  7. CChildView::DrawMandelbrot 메서드를 구현합니다. 이 메서드는 지정된 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. CChildView::OnPaint 메서드를 구현합니다. 이 메서드는 개체의 Bitmap 내용을 호출 CChildView::DrawMandelbrot 한 다음 창에 복사합니다.

    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. 애플리케이션을 빌드하고 실행하여 애플리케이션이 성공적으로 업데이트되었는지 확인합니다.

다음 그림에서는 Mandelbrot 애플리케이션의 결과를 보여 줍니다.

The Mandelbrot Application.

각 픽셀에 대한 계산 비용이 계산 비용이 많이 들기 때문에 UI 스레드는 전체 계산이 완료될 때까지 추가 메시지를 처리할 수 없습니다. 이렇게 하면 애플리케이션의 응답성이 저하 될 수 있습니다. 그러나 UI 스레드에서 작업을 제거하여 이 문제를 완화할 수 있습니다.

[맨 위로 이동]

UI 스레드에서 작업 제거

이 섹션에서는 Mandelbrot 애플리케이션의 UI 스레드에서 그리기 작업을 제거하는 방법을 보여줍니다. UI 스레드에서 작업자 스레드로 그리기 작업을 이동하면 작업자 스레드가 백그라운드에서 이미지를 생성할 때 UI 스레드가 메시지를 처리할 수 있습니다.

동시성 런타임은 태스크 그룹, 비동기 에이전트 및 간단한 작업의 세 가지 방법으로 작업을 실행할 수 있습니다. 이러한 메커니즘 중 하나를 사용하여 UI 스레드에서 작업을 제거할 수 있지만 작업 그룹이 취소를 지원하므로 이 예제에서는 동시성::task_group 개체를 사용합니다. 이 연습에서는 나중에 취소를 사용하여 클라이언트 창의 크기를 조정할 때 수행되는 작업의 양을 줄이고 창이 제거될 때 클린up을 수행합니다.

이 예제에서는 동시성::unbounded_buffer 개체를 사용하여 UI 스레드와 작업자 스레드가 서로 통신할 수 있도록 합니다. 작업자 스레드가 이미지를 생성한 후 개체에 Bitmap 대한 포인터를 개체로 unbounded_buffer 보낸 다음 페인트 메시지를 UI 스레드에 게시합니다. 그런 다음 UI 스레드는 개체에서 unbounded_buffer 개체를 Bitmap 수신하고 클라이언트 창에 그립니다.

UI 스레드에서 그리기 작업을 제거하려면

  1. pch.h(Visual Studio 2017 이하의 stdafx.h)에서 다음 #include 지시문을 추가합니다.

    #include <agents.h>
    #include <ppl.h>
    
  2. ChildView.h에서 클래스의 섹션에 멤버 변수를 protected 추가하고 task_groupunbounded_buffer 추가합니다CChildView. 개체는 task_group 그리기를 수행하는 작업을 보유합니다. 개체는 unbounded_buffer 완성된 만델브로트 이미지를 보유합니다.

    concurrency::task_group m_DrawingTasks;
    concurrency::unbounded_buffer<BitmapPtr> m_MandelbrotImages;
    
  3. ChildView.cpp에서 네임스페이 using 스에 지시문을 concurrency 추가합니다.

    using namespace concurrency;
    
  4. 메서드에서 CChildView::DrawMandelbrot 호출 Bitmap::UnlockBits후 concurrency::send 함수를 호출하여 개체를 Bitmap UI 스레드에 전달합니다. 그런 다음 UI 스레드에 페인트 메시지를 게시하고 클라이언트 영역을 무효화합니다.

    // 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. 업데이트된 CChildView::OnPaint 개체를 수신하도록 메서드를 업데이트 Bitmap 하고 클라이언트 창에 이미지를 그립니다.

    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)));
          });
       }
    }
    

    이 메서드는 CChildView::OnPaint 메시지 버퍼에 없는 경우 Mandelbrot 이미지를 생성하는 작업을 만듭니다. 메시지 버퍼는 초기 페인트 메시지와 같은 경우와 클라이언트 창 앞에서 다른 창을 이동할 때 개체를 포함하지 Bitmap 않습니다.

  6. 애플리케이션을 빌드하고 실행하여 애플리케이션이 성공적으로 업데이트되었는지 확인합니다.

이제 그리기 작업이 백그라운드에서 수행되므로 UI의 응답성이 향상되었습니다.

[맨 위로 이동]

그리기 성능 향상

각 픽셀의 계산은 다른 모든 계산과 독립적이므로 만델브로트 프랙탈의 생성은 병렬화에 적합한 후보입니다. 그리기 프로시저를 병렬화하려면 다음과 같이 메서드의 CChildView::DrawMandelbrot 외부 for 루프를 concurrency::p arallel_for 알고리즘에 대한 호출로 변환합니다.

// Compute whether each point lies in the Mandelbrot set.
parallel_for (0u, height, [&](UINT row)
{
   // Loop body omitted for brevity.
});

각 비트맵 요소의 계산은 독립적이므로 비트맵 메모리에 액세스하는 그리기 작업을 동기화할 필요가 없습니다. 이렇게 하면 사용 가능한 프로세서 수가 증가함에 따라 성능이 확장됩니다.

[맨 위로 이동]

취소에 대한 지원 추가

이 섹션에서는 창 크기 조정을 처리하는 방법과 창이 제거될 때 활성 드로잉 작업을 취소하는 방법을 설명합니다.

PPL의 문서 취소는 런타임에서 취소가 작동하는 방식을 설명합니다. 취소는 협조적입니다. 따라서 즉시 발생하지 않습니다. 취소된 작업을 중지하기 위해 런타임은 태스크에서 런타임으로 후속 호출하는 동안 내부 예외를 throw합니다. 이전 섹션에서는 알고리즘을 parallel_for 사용하여 그리기 작업의 성능을 향상시키는 방법을 보여 줍니다. 이 호출을 parallel_for 사용하면 런타임에서 작업을 중지할 수 있으므로 취소가 작동할 수 있습니다.

활성 작업 취소

Mandelbrot 애플리케이션은 차원이 클라이언트 창의 크기와 일치하는 개체를 만듭니다 Bitmap . 클라이언트 창의 크기를 조정할 때마다 애플리케이션은 새 창 크기에 대한 이미지를 생성하는 추가 백그라운드 작업을 만듭니다. 애플리케이션에는 이러한 중간 이미지가 필요하지 않습니다. 최종 창 크기에 대한 이미지만 필요합니다. 애플리케이션이 이 추가 작업을 수행하지 못하도록 하려면 메시지 처리기 및 메시지에 대한 WM_SIZEWM_SIZING 활성 그리기 작업을 취소한 다음 창 크기가 조정된 후 그리기 작업 일정을 다시 지정할 수 있습니다.

창 크기가 조정될 때 활성 그리기 작업을 취소하기 위해 애플리케이션은 해당 및 WM_SIZE 메시지의 처리기에서 동시성::task_group::cancel 메서드를 WM_SIZING 호출합니다. 또한 메시지 처리 WM_SIZE 기는 동시성::task_group::wait 메서드를 호출하여 모든 활성 작업이 완료될 때까지 기다린 다음 업데이트된 창 크기에 대한 그리기 작업 일정을 다시 예약합니다.

클라이언트 창이 제거되면 활성 그리기 작업을 취소하는 것이 좋습니다. 활성 그리기 작업을 취소하면 클라이언트 창이 제거된 후 작업자 스레드가 UI 스레드에 메시지를 게시하지 않도록 합니다. 애플리케이션은 메시지 처리기의 활성 그리기 작업을 취소합니다 WM_DESTROY .

취소에 응답

그리기 작업을 수행하는 메서드는 CChildView::DrawMandelbrot 취소에 응답해야 합니다. 런타임에서 예외 처리를 사용하여 작업을 취소하기 때문에 메서드는 CChildView::DrawMandelbrot 예외로부터 안전한 메커니즘을 사용하여 모든 리소스가 올바르게 클린 보장해야 합니다. 이 예제에서는 RAII(리소스 취득 초기화) 패턴을 사용하여 작업이 취소될 때 비트맵 비트의 잠금이 해제되도록 합니다.

Mandelbrot 애플리케이션에서 취소에 대한 지원을 추가하려면
  1. ChildView.h protectedCChildView 클래스 섹션에서 , OnSizingOnDestroy 메시지 맵 함수에 OnSize대한 선언을 추가합니다.

    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. ChildView.cpp에서 , 및 WM_DESTROY 메시지에 대한 처리기를 포함하도록 메시지 맵을 WM_SIZEWM_SIZING수정합니다.

    BEGIN_MESSAGE_MAP(CChildView, CWnd)
       ON_WM_PAINT()
       ON_WM_SIZE()
       ON_WM_SIZING()
       ON_WM_DESTROY()
    END_MESSAGE_MAP()
    
  3. CChildView::OnSizing 메서드를 구현합니다. 이 메서드는 기존 그리기 작업을 취소합니다.

    void CChildView::OnSizing(UINT nSide, LPRECT lpRect)
    {
       // The window size is changing; cancel any existing drawing tasks.
       m_DrawingTasks.cancel();
    }
    
  4. CChildView::OnSize 메서드를 구현합니다. 이 메서드는 기존 그리기 작업을 취소하고 업데이트된 클라이언트 창 크기에 대한 새 그리기 작업을 만듭니다.

    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. CChildView::OnDestroy 메서드를 구현합니다. 이 메서드는 기존 그리기 작업을 취소합니다.

    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. ChildView.cpp에서 RAII 패턴을 구현하는 클래스를 정의 scope_guard 합니다.

    // 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. 호출 후 메서드에 CChildView::DrawMandelbrot 다음 코드를 추가합니다 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);      
    });
    

    이 코드는 개체를 만들어 취소를 scope_guard 처리합니다. 개체가 범위를 벗어나면 비트맵 비트의 잠금이 해제됩니다.

  8. 비트맵 비트가 CChildView::DrawMandelbrot 잠금 해제된 후 UI 스레드로 메시지를 보내기 전에 개체를 해제 scope_guard 하도록 메서드의 끝을 수정합니다. 이렇게 하면 비트맵 비트가 잠금 해제되기 전에 UI 스레드가 업데이트되지 않습니다.

    // 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. 애플리케이션을 빌드하고 실행하여 애플리케이션이 성공적으로 업데이트되었는지 확인합니다.

창 크기를 조정하면 그리기 작업은 최종 창 크기에 대해서만 수행됩니다. 창이 제거되면 활성 그리기 작업도 취소됩니다.

[맨 위로 이동]

참고 항목

동시성 런타임 연습
작업 병렬 처리
비동기 메시지 블록
메시지 전달 함수
병렬 알고리즘
PPL에서의 취소
MFC 데스크톱 응용 프로그램