Panduan: Menghapus Pekerjaan dari Utas Antarmuka Pengguna

Dokumen ini menunjukkan cara menggunakan Runtime Konkurensi untuk memindahkan pekerjaan yang dilakukan oleh utas antarmuka pengguna (UI) di aplikasi Microsoft Foundation Classes (MFC) ke utas pekerja. Dokumen ini juga menunjukkan cara meningkatkan performa operasi gambar yang panjang.

Menghapus pekerjaan dari utas UI dengan membongkar operasi pemblokiran, misalnya, menggambar, ke utas pekerja dapat meningkatkan responsivitas aplikasi Anda. Panduan ini menggunakan rutinitas menggambar yang menghasilkan fraktal Mandelbrot untuk menunjukkan operasi pemblokiran yang panjang. Generasi fraktal Mandelbrot juga merupakan kandidat yang baik untuk paralelisasi karena komputasi setiap piksel tidak bergantung pada semua komputasi lainnya.

Prasyarat

Baca topik berikut sebelum Anda memulai panduan ini:

Kami juga menyarankan agar Anda memahami dasar-dasar pengembangan aplikasi MFC dan GDI+ sebelum Anda memulai panduan ini. Untuk informasi selengkapnya tentang MFC, lihat Aplikasi Desktop MFC. Untuk informasi selengkapnya tentang GDI+, lihat GDI+.

Bagian

Panduan ini berisi bagian berikut:

Membuat Aplikasi MFC

Bagian ini menjelaskan cara membuat aplikasi MFC dasar.

Untuk membuat aplikasi Visual C++ MFC

  1. Gunakan Wizard Aplikasi MFC untuk membuat aplikasi MFC dengan semua pengaturan default. Lihat Panduan: Menggunakan Kontrol Shell MFC Baru untuk petunjuk tentang cara membuka wizard untuk versi Visual Studio Anda.

  2. Ketik nama untuk proyek, misalnya, Mandelbrot, lalu klik OK untuk menampilkan Panduan Aplikasi MFC.

  3. Di panel Jenis Aplikasi, pilih Dokumen tunggal. Pastikan bahwa kotak centang dukungan arsitektur Dokumen/Tampilan dikosongkan.

  4. Klik Selesai untuk membuat proyek dan menutup MFC Application Wizard.

    Verifikasi bahwa aplikasi berhasil dibuat dengan membangun dan menjalankannya. Untuk membuat aplikasi, pada menu Build , klik Bangun Solusi. Jika aplikasi berhasil dibangun, jalankan aplikasi dengan mengklik Mulai Penelusuran Kesalahan pada menu Debug .

Menerapkan Versi Serial Aplikasi Mandelbrot

Bagian ini menjelaskan cara menggambar fraktal Mandelbrot. Versi ini menggambar fraktal Mandelbrot ke objek GDI+ Bitmap dan kemudian menyalin konten bitmap tersebut ke jendela klien.

Untuk mengimplementasikan versi serial aplikasi Mandelbrot

  1. Di pch.h (stdafx.h di Visual Studio 2017 dan yang lebih lama), tambahkan arahan berikut #include :

    #include <memory>
    
  2. Di ChildView.h, setelah direktif pragma , tentukan jenisnya BitmapPtr . Jenis ini BitmapPtr memungkinkan penunjuk ke Bitmap objek untuk dibagikan oleh beberapa komponen. Objek Bitmap dihapus ketika tidak lagi dirujuk oleh komponen apa pun.

    typedef std::shared_ptr<Gdiplus::Bitmap> BitmapPtr;
    
  3. Di ChildView.h, tambahkan kode berikut ke bagian protectedCChildView kelas:

    protected:
       // Draws the Mandelbrot fractal to the specified Bitmap object.
       void DrawMandelbrot(BitmapPtr);
    
    protected:
       ULONG_PTR m_gdiplusToken;
    
  4. Di ChildView.cpp, komentari atau hapus baris berikut.

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

    Dalam build Debug, langkah ini mencegah aplikasi menggunakan DEBUG_NEW alokator, yang tidak kompatibel dengan GDI+.

  5. Di ChildView.cpp, tambahkan using direktif ke Gdiplus namespace.

    using namespace Gdiplus;
    
  6. Tambahkan kode berikut ke konstruktor dan destruktor CChildView kelas untuk menginisialisasi dan mematikan GDI+.

    CChildView::CChildView()
    {
       // Initialize GDI+.
       GdiplusStartupInput gdiplusStartupInput;
       GdiplusStartup(&m_gdiplusToken, &gdiplusStartupInput, NULL);
    }
    
    CChildView::~CChildView()
    {
       // Shutdown GDI+.
       GdiplusShutdown(m_gdiplusToken);
    }
    
  7. Mengimplementasikan metode CChildView::DrawMandelbrot. Metode ini menggambar fraktal Mandelbrot ke objek yang ditentukan 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. Mengimplementasikan metode CChildView::OnPaint. Metode ini memanggil CChildView::DrawMandelbrot lalu menyalin konten Bitmap objek ke jendela.

    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. Verifikasi bahwa aplikasi berhasil diperbarui dengan membangun dan menjalankannya.

Ilustrasi berikut menunjukkan hasil aplikasi Mandelbrot.

The Mandelbrot Application.

Karena komputasi untuk setiap piksel secara komputasi mahal, utas UI tidak dapat memproses pesan tambahan hingga komputasi keseluruhan selesai. Ini dapat mengurangi responsivitas dalam aplikasi. Namun, Anda dapat meringankan masalah ini dengan menghapus pekerjaan dari utas UI.

[Atas]

Menghapus Pekerjaan dari UI Thread

Bagian ini menunjukkan cara menghapus pekerjaan menggambar dari utas UI di aplikasi Mandelbrot. Dengan memindahkan pekerjaan menggambar dari utas UI ke utas pekerja, utas UI dapat memproses pesan saat utas pekerja menghasilkan gambar di latar belakang.

Runtime Konkurensi menyediakan tiga cara untuk menjalankan tugas: grup tugas, agen asinkron, dan tugas ringan. Meskipun Anda dapat menggunakan salah satu mekanisme ini untuk menghapus pekerjaan dari utas UI, contoh ini menggunakan objek konkurensi::task_group karena grup tugas mendukung pembatalan. Panduan ini nantinya menggunakan pembatalan untuk mengurangi jumlah pekerjaan yang dilakukan ketika jendela klien diubah ukurannya, dan untuk melakukan pembersihan saat jendela dihancurkan.

Contoh ini juga menggunakan objek konkurensi::unbounded_buffer untuk mengaktifkan utas UI dan utas pekerja untuk berkomunikasi satu sama lain. Setelah utas pekerja menghasilkan gambar, utas tersebut mengirimkan penunjuk ke Bitmap objek ke unbounded_buffer objek lalu memposting pesan cat ke utas UI. Utas UI kemudian menerima dari unbounded_buffer objek Bitmap objek dan menggambarnya ke jendela klien.

Untuk menghapus pekerjaan menggambar dari utas UI

  1. Di pch.h (stdafx.h di Visual Studio 2017 dan yang lebih lama), tambahkan arahan berikut #include :

    #include <agents.h>
    #include <ppl.h>
    
  2. Di ChildView.h, tambahkan task_group variabel anggota dan unbounded_buffer ke bagian protectedCChildView kelas. Objek task_group menyimpan tugas yang melakukan gambar; unbounded_buffer objek menyimpan gambar Mandelbrot yang telah selesai.

    concurrency::task_group m_DrawingTasks;
    concurrency::unbounded_buffer<BitmapPtr> m_MandelbrotImages;
    
  3. Di ChildView.cpp, tambahkan using direktif ke concurrency namespace.

    using namespace concurrency;
    
  4. CChildView::DrawMandelbrot Dalam metode , setelah panggilan ke Bitmap::UnlockBits, panggil fungsi konkurensi::kirim untuk meneruskan Bitmap objek ke utas UI. Kemudian posting pesan cat ke utas UI dan batalkan area klien.

    // 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 Perbarui metode untuk menerima objek yang diperbarui Bitmap dan gambar ke jendela klien.

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

    Metode ini CChildView::OnPaint membuat tugas untuk menghasilkan gambar Mandelbrot jika tidak ada di buffer pesan. Buffer pesan tidak akan berisi Bitmap objek dalam kasus seperti pesan cat awal dan ketika jendela lain dipindahkan di depan jendela klien.

  6. Verifikasi bahwa aplikasi berhasil diperbarui dengan membangun dan menjalankannya.

UI sekarang lebih responsif karena pekerjaan menggambar dilakukan di latar belakang.

[Atas]

Meningkatkan Performa Menggambar

Generasi fraktal Mandelbrot adalah kandidat yang baik untuk paralelisasi karena komputasi setiap piksel independen dari semua komputasi lainnya. Untuk menyejajarkan prosedur gambar, konversikan perulangan luar for dalam CChildView::DrawMandelbrot metode menjadi panggilan ke algoritma konkurensi::p arallel_for , sebagai berikut.

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

Karena komputasi setiap elemen bitmap independen, Anda tidak perlu menyinkronkan operasi gambar yang mengakses memori bitmap. Ini memungkinkan performa untuk menskalakan saat jumlah prosesor yang tersedia meningkat.

[Atas]

Menambahkan Dukungan untuk Pembatalan

Bagian ini menjelaskan cara menangani perubahan ukuran jendela dan cara membatalkan tugas gambar aktif saat jendela dihancurkan.

Dokumen Pembatalan dalam PPL menjelaskan cara kerja pembatalan dalam runtime. Pembatalan bersifat kooperatif; oleh karena itu, itu tidak segera terjadi. Untuk menghentikan tugas yang dibatalkan, runtime melempar pengecualian internal selama panggilan berikutnya dari tugas ke dalam runtime. Bagian sebelumnya memperlihatkan cara menggunakan parallel_for algoritma untuk meningkatkan performa tugas menggambar. Panggilan untuk parallel_for memungkinkan runtime untuk menghentikan tugas, dan karena itu memungkinkan pembatalan berfungsi.

Membatalkan Tugas Aktif

Aplikasi Mandelbrot membuat Bitmap objek yang dimensinya cocok dengan ukuran jendela klien. Setiap kali jendela klien diubah ukurannya, aplikasi membuat tugas latar belakang tambahan untuk menghasilkan gambar untuk ukuran jendela baru. Aplikasi tidak memerlukan gambar perantara ini; hanya memerlukan gambar untuk ukuran jendela akhir. Untuk mencegah aplikasi melakukan pekerjaan tambahan ini, Anda dapat membatalkan tugas gambar aktif apa pun di penangan pesan untuk WM_SIZE pesan dan WM_SIZING lalu menjadwalkan ulang pekerjaan menggambar setelah jendela diubah ukurannya.

Untuk membatalkan tugas gambar aktif saat jendela diubah ukurannya, aplikasi memanggil metode konkurensi::task_group::cancel di handler untuk WM_SIZING pesan dan WM_SIZE . Handler untuk WM_SIZE pesan juga memanggil metode concurrency::task_group::wait untuk menunggu semua tugas aktif selesai lalu menjadwalkan ulang tugas menggambar untuk ukuran jendela yang diperbarui.

Ketika jendela klien dihancurkan, ada baiknya untuk membatalkan tugas gambar aktif apa pun. Membatalkan tugas gambar aktif memastikan bahwa utas pekerja tidak memposting pesan ke utas UI setelah jendela klien dihancurkan. Aplikasi membatalkan tugas gambar aktif apa pun di handler untuk pesan.WM_DESTROY

Menanggapi Pembatalan

Metode CChildView::DrawMandelbrot , yang melakukan tugas menggambar, harus merespons pembatalan. Karena runtime menggunakan penanganan pengecualian untuk membatalkan tugas, CChildView::DrawMandelbrot metode harus menggunakan mekanisme yang aman pengecualian untuk menjamin bahwa semua sumber daya dibersihkan dengan benar. Contoh ini menggunakan pola Resource Acquisition Is Initialization (RAII) untuk menjamin bahwa bit bitmap tidak terkunci saat tugas dibatalkan.

Untuk menambahkan dukungan untuk pembatalan di aplikasi Mandelbrot
  1. Di ChildView.h, di bagian protectedCChildView kelas, tambahkan deklarasi untuk OnSizefungsi peta pesan , , OnSizingdan 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. Di ChildView.cpp, ubah peta pesan agar berisi handler untuk WM_SIZEpesan , , WM_SIZINGdan WM_DESTROY .

    BEGIN_MESSAGE_MAP(CChildView, CWnd)
       ON_WM_PAINT()
       ON_WM_SIZE()
       ON_WM_SIZING()
       ON_WM_DESTROY()
    END_MESSAGE_MAP()
    
  3. Mengimplementasikan metode CChildView::OnSizing. Metode ini membatalkan tugas menggambar yang ada.

    void CChildView::OnSizing(UINT nSide, LPRECT lpRect)
    {
       // The window size is changing; cancel any existing drawing tasks.
       m_DrawingTasks.cancel();
    }
    
  4. Mengimplementasikan metode CChildView::OnSize. Metode ini membatalkan tugas menggambar yang ada dan membuat tugas gambar baru untuk ukuran jendela klien yang diperbarui.

    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. Mengimplementasikan metode CChildView::OnDestroy. Metode ini membatalkan tugas menggambar yang ada.

    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. Di ChildView.cpp, tentukan scope_guard kelas , yang mengimplementasikan pola 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. Tambahkan kode berikut ke CChildView::DrawMandelbrot metode setelah panggilan ke 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);      
    });
    

    Kode ini menangani pembatalan dengan membuat scope_guard objek. Ketika objek meninggalkan cakupan, objek akan membuka bit bitmap.

  8. Ubah akhir CChildView::DrawMandelbrot metode untuk menutup scope_guard objek setelah bit bitmap tidak terkunci, tetapi sebelum pesan dikirim ke utas UI. Ini memastikan bahwa utas UI tidak diperbarui sebelum bitmap dibuka kuncinya.

    // 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. Verifikasi bahwa aplikasi berhasil diperbarui dengan membangun dan menjalankannya.

Saat Anda mengubah ukuran jendela, pekerjaan menggambar hanya dilakukan untuk ukuran jendela akhir. Setiap tugas gambar aktif juga dibatalkan ketika jendela dihancurkan.

[Atas]

Baca juga

Panduan Runtime Konkurensi
Paralelisme Tugas
Blok Pesan Asinkron
Fungsi Passing Pesan
Algoritma Paralel
Pembatalan di PPL
Aplikasi Desktop MFC