Praktik Terbaik di Pustaka Pola Paralel

Dokumen ini menjelaskan cara terbaik untuk memanfaatkan Pustaka Pola Paralel (PPL) yang efektif. PPL menyediakan kontainer, objek, dan algoritma tujuan umum untuk melakukan paralelisme mendetail.

Untuk informasi selengkapnya tentang PPL, lihat Pustaka Pola Paralel (PPL).

Bagian

Dokumen ini berisi bagian berikut:

Jangan Sejajarkan Badan Perulangan Kecil

Paralelisasi badan perulangan yang relatif kecil dapat menyebabkan overhead penjadwalan terkait melebihi manfaat pemrosesan paralel. Pertimbangkan contoh berikut, yang menambahkan setiap pasangan elemen dalam dua array.

// small-loops.cpp
// compile with: /EHsc
#include <ppl.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain()
{
   // Create three arrays that each have the same size.
   const size_t size = 100000;
   int a[size], b[size], c[size];

   // Initialize the arrays a and b.
   for (size_t i = 0; i < size; ++i)
   {
      a[i] = i;
      b[i] = i * 2;
   }

   // Add each pair of elements in arrays a and b in parallel 
   // and store the result in array c.
   parallel_for<size_t>(0, size, [&a,&b,&c](size_t i) {
      c[i] = a[i] + b[i];
   });

   // TODO: Do something with array c.
}

Beban kerja untuk setiap iterasi perulangan paralel terlalu kecil untuk mendapatkan manfaat dari overhead untuk pemrosesan paralel. Anda dapat meningkatkan performa perulangan ini dengan melakukan lebih banyak pekerjaan dalam bodi perulangan atau dengan melakukan perulangan secara serial.

[Atas]

Paralelisme Ekspres pada Tingkat Setinggi Mungkin

Ketika Anda paralelisasi kode hanya pada tingkat rendah, Anda dapat memperkenalkan konstruksi fork-join yang tidak menskalakan saat jumlah prosesor meningkat. Konstruksi fork-join adalah konstruksi di mana satu tugas membagi pekerjaannya menjadi subtugas paralel yang lebih kecil dan menunggu subtugas tersebut selesai. Setiap subtugas dapat membagi dirinya secara rekursif menjadi subtugas tambahan.

Meskipun model fork-join dapat berguna untuk memecahkan berbagai masalah, ada situasi di mana overhead sinkronisasi dapat mengurangi skalabilitas. Misalnya, pertimbangkan kode serial berikut yang memproses data gambar.

// Calls the provided function for each pixel in a Bitmap object.
void ProcessImage(Bitmap* bmp, const function<void (DWORD&)>& f)
{
   int width = bmp->GetWidth();
   int height = bmp->GetHeight();

   // Lock the bitmap.
   BitmapData bitmapData;
   Rect rect(0, 0, bmp->GetWidth(), bmp->GetHeight());
   bmp->LockBits(&rect, ImageLockModeWrite, PixelFormat32bppRGB, &bitmapData);

   // Get a pointer to the bitmap data.
   DWORD* image_bits = (DWORD*)bitmapData.Scan0;

   // Call the function for each pixel in the image.
   for (int y = 0; y < height; ++y)
   {      
      for (int x = 0; x < width; ++x)
      {
         // Get the current pixel value.
         DWORD* curr_pixel = image_bits + (y * width) + x;

         // Call the function.
         f(*curr_pixel);
      }
   }

   // Unlock the bitmap.
   bmp->UnlockBits(&bitmapData);
}

Karena setiap iterasi perulangan independen, Anda dapat menyejajarkan banyak pekerjaan, seperti yang ditunjukkan dalam contoh berikut. Contoh ini menggunakan konkurensi::p arallel_for algoritma untuk menyejajarkan perulangan luar.

// Calls the provided function for each pixel in a Bitmap object.
void ProcessImage(Bitmap* bmp, const function<void (DWORD&)>& f)
{
   int width = bmp->GetWidth();
   int height = bmp->GetHeight();

   // Lock the bitmap.
   BitmapData bitmapData;
   Rect rect(0, 0, bmp->GetWidth(), bmp->GetHeight());
   bmp->LockBits(&rect, ImageLockModeWrite, PixelFormat32bppRGB, &bitmapData);

   // Get a pointer to the bitmap data.
   DWORD* image_bits = (DWORD*)bitmapData.Scan0;

   // Call the function for each pixel in the image.
   parallel_for (0, height, [&, width](int y)
   {      
      for (int x = 0; x < width; ++x)
      {
         // Get the current pixel value.
         DWORD* curr_pixel = image_bits + (y * width) + x;

         // Call the function.
         f(*curr_pixel);
      }
   });

   // Unlock the bitmap.
   bmp->UnlockBits(&bitmapData);
}

Contoh berikut mengilustrasikan konstruksi fork-join dengan memanggil ProcessImage fungsi dalam perulangan. Setiap panggilan ke ProcessImage tidak kembali hingga setiap subtugas selesai.

// Processes each bitmap in the provided vector.
void ProcessImages(vector<Bitmap*> bitmaps, const function<void (DWORD&)>& f)
{
   for_each(begin(bitmaps), end(bitmaps), [&f](Bitmap* bmp) {
      ProcessImage(bmp, f);
   });
}

Jika setiap perulangan perulangan paralel melakukan hampir tidak ada pekerjaan, atau pekerjaan yang dilakukan oleh perulangan paralel tidak seimbang, artinya, beberapa perulangan perulangan membutuhkan waktu lebih lama daripada yang lain, overhead penjadwalan yang diperlukan untuk sering fork dan menggabungkan pekerjaan dapat melebihi manfaat untuk eksekusi paralel. Overhead ini meningkat saat jumlah prosesor meningkat.

Untuk mengurangi jumlah overhead penjadwalan dalam contoh ini, Anda dapat menyejajarkan perulangan luar sebelum Anda menyejajarkan perulangan dalam atau menggunakan konstruksi paralel lain seperti pipelining. Contoh berikut memodifikasi ProcessImages fungsi untuk menggunakan algoritma konkurensi::p arallel_for_each untuk paralelisasi perulangan luar.

// Processes each bitmap in the provided vector.
void ProcessImages(vector<Bitmap*> bitmaps, const function<void (DWORD&)>& f)
{
   parallel_for_each(begin(bitmaps), end(bitmaps), [&f](Bitmap* bmp) {
      ProcessImage(bmp, f);
   });
}

Untuk contoh serupa yang menggunakan alur untuk melakukan pemrosesan gambar secara paralel, lihat Panduan: Membuat Jaringan Pemrosesan Gambar.

[Atas]

Gunakan parallel_invoke untuk Menyelesaikan Masalah Pembagian dan Penaklukan

Masalah pembagian dan penaklukan adalah bentuk konstruksi fork-join yang menggunakan rekursi untuk memecah tugas menjadi subtugas. Selain konkurensi::task_group dan konkurensi::structured_task_group kelas, Anda juga dapat menggunakan algoritma konkurensi::p arallel_invoke untuk menyelesaikan masalah pembagian dan penaklukan. parallel_invoke Algoritma memiliki sintaks yang lebih succinct daripada objek grup tugas, dan berguna ketika Anda memiliki jumlah tugas paralel tetap.

Contoh berikut mengilustrasikan penggunaan parallel_invoke algoritma untuk mengimplementasikan algoritma pengurutan bitonik.

// Sorts the given sequence in the specified order.
template <class T>
void parallel_bitonic_sort(T* items, int lo, int n, bool dir)
{   
   if (n > 1)
   {
      // Divide the array into two partitions and then sort 
      // the partitions in different directions.
      int m = n / 2;

      parallel_invoke(
         [&] { parallel_bitonic_sort(items, lo, m, INCREASING); },
         [&] { parallel_bitonic_sort(items, lo + m, m, DECREASING); }
      );
      
      // Merge the results.
      parallel_bitonic_merge(items, lo, n, dir);
   }
}

Untuk mengurangi overhead, parallel_invoke algoritma melakukan rangkaian tugas terakhir pada konteks panggilan.

Untuk versi lengkap contoh ini, lihat Cara: Menggunakan parallel_invoke untuk Menulis Rutinitas Pengurutan Paralel. Untuk informasi selengkapnya tentang parallel_invoke algoritma, lihat Algoritma Paralel.

[Atas]

Gunakan Penanganan Pembatalan atau Pengecualian untuk Memisahkan dari Perulangan Paralel

PPL menyediakan dua cara untuk membatalkan pekerjaan paralel yang dilakukan oleh grup tugas atau algoritma paralel. Salah satu caranya adalah dengan menggunakan mekanisme pembatalan yang disediakan oleh kelas konkurensi::task_group dan konkurensi::structured_task_group . Cara lain adalah dengan melemparkan pengecualian dalam isi fungsi kerja tugas. Mekanisme pembatalan lebih efisien daripada penanganan pengecualian saat membatalkan pohon pekerjaan paralel. Pohon kerja paralel adalah sekelompok grup tugas terkait di mana beberapa grup tugas berisi grup tugas lainnya. Mekanisme pembatalan membatalkan grup tugas dan grup tugas turunannya dengan cara top-down. Sebaliknya, penanganan pengecualian bekerja dengan cara bawah ke atas dan harus membatalkan setiap grup tugas turunan secara independen saat pengecualian menyebar ke atas.

Saat Anda bekerja langsung dengan objek grup tugas, gunakan metode konkurensi::task_group::cancel atau konkurensi::structured_task_group::batal untuk membatalkan pekerjaan yang termasuk dalam grup tugas tersebut. Untuk membatalkan algoritma paralel, misalnya, parallel_for, buat grup tugas induk dan batalkan grup tugas tersebut. Misalnya, pertimbangkan fungsi berikut, parallel_find_any, yang mencari nilai dalam array secara paralel.

// Returns the position in the provided array that contains the given value, 
// or -1 if the value is not in the array.
template<typename T>
int parallel_find_any(const T a[], size_t count, const T& what)
{
   // The position of the element in the array. 
   // The default value, -1, indicates that the element is not in the array.
   int position = -1;

   // Call parallel_for in the context of a cancellation token to search for the element.
   cancellation_token_source cts;
   run_with_cancellation_token([count, what, &a, &position, &cts]()
   {
      parallel_for(std::size_t(0), count, [what, &a, &position, &cts](int n) {
         if (a[n] == what)
         {
            // Set the return value and cancel the remaining tasks.
            position = n;
            cts.cancel();
         }
      });
   }, cts.get_token());

   return position;
}

Karena algoritma paralel menggunakan grup tugas, ketika salah satu perulangan paralel membatalkan grup tugas induk, tugas keseluruhan dibatalkan. Untuk versi lengkap contoh ini, lihat Cara: Menggunakan Pembatalan untuk Memutuskan dari Perulangan Paralel.

Meskipun penanganan pengecualian adalah cara yang kurang efisien untuk membatalkan pekerjaan paralel daripada mekanisme pembatalan, ada kasus di mana penanganan pengecualian sesuai. Misalnya, metode berikut, for_all, secara rekursif melakukan fungsi kerja pada setiap simpul tree struktur. Dalam contoh ini, _children anggota data adalah std::list yang berisi tree objek.

// Performs the given work function on the data element of the tree and
// on each child.
template<class Function>
void tree::for_all(Function& action)
{
   // Perform the action on each child.
   parallel_for_each(begin(_children), end(_children), [&](tree& child) {
      child.for_all(action);
   });

   // Perform the action on this node.
   action(*this);
}

Pemanggil tree::for_all metode dapat melemparkan pengecualian jika tidak memerlukan fungsi kerja untuk dipanggil pada setiap elemen pohon. Contoh berikut menunjukkan search_for_value fungsi, yang mencari nilai dalam objek yang disediakan tree . Fungsi ini search_for_value menggunakan fungsi kerja yang melempar pengecualian ketika elemen pohon saat ini cocok dengan nilai yang disediakan. Fungsi ini search_for_value menggunakan try-catch blok untuk menangkap pengecualian dan mencetak hasilnya ke konsol.

// Searches for a value in the provided tree object.
template <typename T>
void search_for_value(tree<T>& t, int value)
{
   try
   {
      // Call the for_all method to search for a value. The work function
      // throws an exception when it finds the value.
      t.for_all([value](const tree<T>& node) {
         if (node.get_data() == value)
         {
            throw &node;
         }
      });
   }
   catch (const tree<T>* node)
   {
      // A matching node was found. Print a message to the console.
      wstringstream ss;
      ss << L"Found a node with value " << value << L'.' << endl;
      wcout << ss.str();
      return;
   }

   // A matching node was not found. Print a message to the console.
   wstringstream ss;
   ss << L"Did not find node with value " << value << L'.' << endl;
   wcout << ss.str();   
}

Untuk versi lengkap contoh ini, lihat Cara: Menggunakan Penanganan Pengecualian untuk Memisahkan dari Perulangan Paralel.

Untuk informasi umum selengkapnya tentang mekanisme pembatalan dan penanganan pengecualian yang disediakan oleh PPL, lihat Pembatalan dalam PPL dan Penanganan Pengecualian.

[Atas]

Memahami bagaimana Penanganan Pembatalan dan Pengecualian Memengaruhi Penghancuran Objek

Di pohon pekerjaan paralel, tugas yang dibatalkan mencegah tugas anak berjalan. Ini dapat menyebabkan masalah jika salah satu tugas anak melakukan operasi yang penting untuk aplikasi Anda, seperti membebaskan sumber daya. Selain itu, pembatalan tugas dapat menyebabkan pengecualian disebarluaskan melalui destruktor objek dan menyebabkan perilaku yang tidak terdefinisi dalam aplikasi Anda.

Dalam contoh berikut, Resource kelas menjelaskan sumber daya dan Container kelas menjelaskan kontainer yang menyimpan sumber daya. Dalam destruktornya, Container kelas memanggil cleanup metode pada dua anggotanya Resource secara paralel dan kemudian memanggil cleanup metode pada anggota ketiganya Resource .

// parallel-resource-destruction.h
#pragma once
#include <ppl.h>
#include <sstream>
#include <iostream>

// Represents a resource.
class Resource
{
public:
   Resource(const std::wstring& name)
      : _name(name)
   {
   }

   // Frees the resource.
   void cleanup()
   {
      // Print a message as a placeholder.
      std::wstringstream ss;
      ss << _name << L": Freeing..." << std::endl;
      std::wcout << ss.str();
   }
private:
   // The name of the resource.
   std::wstring _name;
};

// Represents a container that holds resources.
class Container
{
public:
   Container(const std::wstring& name)
      : _name(name)
      , _resource1(L"Resource 1")
      , _resource2(L"Resource 2")
      , _resource3(L"Resource 3")
   {
   }

   ~Container()
   {
      std::wstringstream ss;
      ss << _name << L": Freeing resources..." << std::endl;
      std::wcout << ss.str();

      // For illustration, assume that cleanup for _resource1
      // and _resource2 can happen concurrently, and that 
      // _resource3 must be freed after _resource1 and _resource2.

      concurrency::parallel_invoke(
         [this]() { _resource1.cleanup(); },
         [this]() { _resource2.cleanup(); }
      );

      _resource3.cleanup();
   }

private:
   // The name of the container.
   std::wstring _name;

   // Resources.
   Resource _resource1;
   Resource _resource2;
   Resource _resource3;
};

Meskipun pola ini tidak memiliki masalah sendiri, pertimbangkan kode berikut yang menjalankan dua tugas secara paralel. Tugas pertama membuat Container objek dan tugas kedua membatalkan tugas keseluruhan. Untuk ilustrasi, contoh menggunakan dua objek konkurensi::peristiwa untuk memastikan bahwa pembatalan terjadi setelah Container objek dibuat dan bahwa Container objek dihancurkan setelah operasi pembatalan terjadi.

// parallel-resource-destruction.cpp
// compile with: /EHsc
#include "parallel-resource-destruction.h"

using namespace concurrency;
using namespace std;

static_assert(false, "This example illustrates a non-recommended practice.");

int main()
{  
   // Create a task_group that will run two tasks.
   task_group tasks;

   // Used to synchronize the tasks.
   event e1, e2;

   // Run two tasks. The first task creates a Container object. The second task
   // cancels the overall task group. To illustrate the scenario where a child 
   // task is not run because its parent task is cancelled, the event objects 
   // ensure that the Container object is created before the overall task is 
   // cancelled and that the Container object is destroyed after the overall 
   // task is cancelled.
   
   tasks.run([&tasks,&e1,&e2] {
      // Create a Container object.
      Container c(L"Container 1");
      
      // Allow the second task to continue.
      e2.set();

      // Wait for the task to be cancelled.
      e1.wait();
   });

   tasks.run([&tasks,&e1,&e2] {
      // Wait for the first task to create the Container object.
      e2.wait();

      // Cancel the overall task.
      tasks.cancel();      

      // Allow the first task to continue.
      e1.set();
   });

   // Wait for the tasks to complete.
   tasks.wait();

   wcout << L"Exiting program..." << endl;
}

Contoh ini menghasilkan output berikut:

Container 1: Freeing resources...Exiting program...

Contoh kode ini berisi masalah berikut yang dapat menyebabkannya berulah berbeda dari yang Anda harapkan:

  • Pembatalan tugas induk menyebabkan tugas anak, panggilan ke konkurensi::p arallel_invoke, juga dibatalkan. Oleh karena itu, kedua sumber daya ini tidak dibeberkan.

  • Pembatalan tugas induk menyebabkan tugas anak melemparkan pengecualian internal. Container Karena destruktor tidak menangani pengecualian ini, pengecualian disebarkan ke atas dan sumber daya ketiga tidak dibebaskan.

  • Pengecualian yang dilemparkan oleh tugas anak menyebar melalui Container destruktor. Melempar dari destruktor menempatkan aplikasi dalam keadaan tidak terdefinisi.

Kami menyarankan agar Anda tidak melakukan operasi penting, seperti membebaskan sumber daya, dalam tugas kecuali Anda dapat menjamin bahwa tugas-tugas ini tidak akan dibatalkan. Kami juga menyarankan agar Anda tidak menggunakan fungsionalitas runtime yang dapat melemparkan destruktor jenis Anda.

[Atas]

Jangan Blokir Berulang Kali dalam Perulangan Paralel

Perulangan paralel seperti konkurensi::p arallel_for atau konkurensi::p arallel_for_each yang didominasi oleh operasi pemblokiran dapat menyebabkan runtime membuat banyak utas dalam waktu singkat.

Runtime Konkurensi melakukan pekerjaan tambahan ketika tugas selesai atau secara kooperatif memblokir atau menghasilkan. Ketika satu blok iterasi perulangan paralel, runtime mungkin memulai iterasi lain. Ketika tidak ada utas diam yang tersedia, runtime membuat utas baru.

Ketika tubuh perulangan paralel sesekali memblokir, mekanisme ini membantu memaksimalkan throughput tugas secara keseluruhan. Namun, ketika banyak perulangan memblokir, runtime dapat membuat banyak utas untuk menjalankan pekerjaan tambahan. Ini dapat menyebabkan kondisi memori rendah atau pemanfaatan sumber daya perangkat keras yang buruk.

Pertimbangkan contoh berikut yang memanggil fungsi konkurensi::send di setiap perulangan perulangan parallel_for . Karena send memblokir secara kooperatif, runtime membuat utas baru untuk menjalankan pekerjaan tambahan setiap kali send dipanggil.

// repeated-blocking.cpp
// compile with: /EHsc
#include <ppl.h>
#include <agents.h>

using namespace concurrency;

static_assert(false, "This example illustrates a non-recommended practice.");

int main()
{
   // Create a message buffer.
   overwrite_buffer<int> buffer;
  
   // Repeatedly send data to the buffer in a parallel loop.
   parallel_for(0, 1000, [&buffer](int i) {
      
      // The send function blocks cooperatively. 
      // We discourage the use of repeated blocking in a parallel
      // loop because it can cause the runtime to create 
      // a large number of threads over a short period of time.
      send(buffer, i);
   });
}

Kami menyarankan agar Anda merefaktor kode Anda untuk menghindari pola ini. Dalam contoh ini, Anda dapat menghindari pembuatan utas tambahan dengan memanggil send dalam perulangan serial for .

[Atas]

Jangan Lakukan Operasi Pemblokiran Saat Anda Membatalkan Pekerjaan Paralel

Jika memungkinkan, jangan lakukan operasi pemblokiran sebelum Anda memanggil konkurensi::task_group::cancel atau konkurensi::structured_task_group::batalkan metode untuk membatalkan pekerjaan paralel.

Ketika tugas melakukan operasi pemblokiran kooperatif, runtime dapat melakukan pekerjaan lain saat tugas pertama menunggu data. Runtime menjadwalkan ulang tugas tunggu saat membuka blokir. Runtime biasanya menjadwalkan ulang tugas yang baru-baru ini tidak diblokir sebelum menjadwalkan ulang tugas yang kurang baru-baru ini tidak diblokir. Oleh karena itu, runtime dapat menjadwalkan pekerjaan yang tidak perlu selama operasi pemblokiran, yang menyebabkan penurunan performa. Dengan demikian, ketika Anda melakukan operasi pemblokiran sebelum Anda membatalkan pekerjaan paralel, operasi pemblokiran dapat menunda panggilan ke cancel. Hal ini menyebabkan tugas lain melakukan pekerjaan yang tidak perlu.

Pertimbangkan contoh berikut yang menentukan parallel_find_answer fungsi, yang mencari elemen array yang disediakan yang memenuhi fungsi predikat yang disediakan. Ketika fungsi predikat mengembalikan true, fungsi kerja paralel membuat Answer objek dan membatalkan tugas keseluruhan.

// blocking-cancel.cpp
// compile with: /c /EHsc
#include <windows.h>
#include <ppl.h>

using namespace concurrency;

// Encapsulates the result of a search operation.
template<typename T>
class Answer
{
public:
   explicit Answer(const T& data)
      : _data(data)
   {
   }

   T get_data() const
   {
      return _data;
   }

   // TODO: Add other methods as needed.

private:
   T _data;

   // TODO: Add other data members as needed.
};

// Searches for an element of the provided array that satisfies the provided
// predicate function.
template<typename T, class Predicate>
Answer<T>* parallel_find_answer(const T a[], size_t count, const Predicate& pred)
{
   // The result of the search.
   Answer<T>* answer = nullptr;
   // Ensures that only one task produces an answer.
   volatile long first_result = 0;

   // Use parallel_for and a task group to search for the element.
   structured_task_group tasks;
   tasks.run_and_wait([&]
   {
      // Declare the type alias for use in the inner lambda function.
      typedef T T;

      parallel_for<size_t>(0, count, [&](const T& n) {
         if (pred(a[n]) && InterlockedExchange(&first_result, 1) == 0)
         {
            // Create an object that holds the answer.
            answer = new Answer<T>(a[n]);
            // Cancel the overall task.
            tasks.cancel();
         }
      });
   });

   return answer;
}

Operator new melakukan alokasi timbunan, yang mungkin memblokir. Runtime melakukan pekerjaan lain hanya ketika tugas melakukan panggilan pemblokiran kooperatif, seperti panggilan ke konkurensi::critical_section::lock.

Contoh berikut menunjukkan cara mencegah pekerjaan yang tidak perlu, dan dengan demikian meningkatkan performa. Contoh ini membatalkan grup tugas sebelum mengalokasikan penyimpanan untuk Answer objek.

// Searches for an element of the provided array that satisfies the provided
// predicate function.
template<typename T, class Predicate>
Answer<T>* parallel_find_answer(const T a[], size_t count, const Predicate& pred)
{
   // The result of the search.
   Answer<T>* answer = nullptr;
   // Ensures that only one task produces an answer.
   volatile long first_result = 0;

   // Use parallel_for and a task group to search for the element.
   structured_task_group tasks;
   tasks.run_and_wait([&]
   {
      // Declare the type alias for use in the inner lambda function.
      typedef T T;

      parallel_for<size_t>(0, count, [&](const T& n) {
         if (pred(a[n]) && InterlockedExchange(&first_result, 1) == 0)
         {
            // Cancel the overall task.
            tasks.cancel();
            // Create an object that holds the answer.
            answer = new Answer<T>(a[n]);            
         }
      });
   });

   return answer;
}

[Atas]

Jangan Menulis ke Data Bersama dalam Perulangan Paralel

Runtime Konkurensi menyediakan beberapa struktur data, misalnya, konkurensi::critical_section, yang menyinkronkan akses bersamaan ke data bersamaan. Struktur data ini berguna dalam banyak kasus, misalnya, ketika beberapa tugas jarang memerlukan akses bersama ke sumber daya.

Pertimbangkan contoh berikut yang menggunakan algoritma konkurensi::p arallel_for_each dan critical_section objek untuk menghitung jumlah angka utama dalam objek std::array . Contoh ini tidak menskalakan karena setiap utas harus menunggu untuk mengakses variabel prime_sumbersama .

critical_section cs;
prime_sum = 0;
parallel_for_each(begin(a), end(a), [&](int i) {
   cs.lock();
   prime_sum += (is_prime(i) ? i : 0);
   cs.unlock();
});

Contoh ini juga dapat menyebabkan performa yang buruk karena operasi penguncian yang sering secara efektif menserialisasikan perulangan. Selain itu, ketika objek Runtime Konkurensi melakukan operasi pemblokiran, penjadwal mungkin membuat utas tambahan untuk melakukan pekerjaan lain saat utas pertama menunggu data. Jika runtime membuat banyak utas karena banyak tugas yang menunggu data bersama, aplikasi dapat berkinerja buruk atau memasukkan status sumber daya rendah.

PPL mendefinisikan kelas konkurensi::combinable , yang membantu Anda menghilangkan status bersama dengan menyediakan akses ke sumber daya bersama dengan cara bebas kunci. Kelas ini combinable menyediakan penyimpanan lokal utas yang memungkinkan Anda melakukan komputasi mendetail lalu menggabungkan komputasi tersebut ke dalam hasil akhir. Anda dapat menganggap combinable objek sebagai variabel pengurangan.

Contoh berikut memodifikasi yang sebelumnya dengan menggunakan combinable objek alih-alih critical_section objek untuk menghitung jumlah. Contoh ini diskalakan karena setiap utas menyimpan salinan lokalnya sendiri dari jumlah tersebut. Contoh ini menggunakan metode concurrency::combinable::combine untuk menggabungkan komputasi lokal ke dalam hasil akhir.

combinable<int> sum;
parallel_for_each(begin(a), end(a), [&](int i) {
   sum.local() += (is_prime(i) ? i : 0);
});
prime_sum = sum.combine(plus<int>());

Untuk versi lengkap contoh ini, lihat Cara: Menggunakan yang dapat dikombinasikan untuk Meningkatkan Performa. Untuk informasi selengkapnya tentang combinable kelas, lihat Kontainer dan Objek Paralel.

[Atas]

Jika memungkinkan, hindari berbagi palsu

Berbagi palsu terjadi ketika beberapa tugas bersamaan yang berjalan pada prosesor terpisah menulis ke variabel yang terletak di baris cache yang sama. Ketika satu tugas menulis ke salah satu variabel, baris cache untuk kedua variabel tidak valid. Setiap prosesor harus memuat ulang baris cache setiap kali baris cache tidak valid. Oleh karena itu, berbagi palsu dapat menyebabkan penurunan performa dalam aplikasi Anda.

Contoh dasar berikut menunjukkan dua tugas bersamaan yang masing-masing menaikkan variabel penghitung bersama.

volatile long count = 0L;
concurrency::parallel_invoke(
   [&count] {
      for(int i = 0; i < 100000000; ++i)
         InterlockedIncrement(&count);
   },
   [&count] {
      for(int i = 0; i < 100000000; ++i)
         InterlockedIncrement(&count);
   }
);

Untuk menghilangkan berbagi data antara dua tugas, Anda dapat memodifikasi contoh untuk menggunakan dua variabel penghitung. Contoh ini menghitung nilai penghitung akhir setelah tugas selesai. Namun, contoh ini menggambarkan berbagi palsu karena variabel count1 dan count2 kemungkinan terletak di baris cache yang sama.

long count1 = 0L;
long count2 = 0L;
concurrency::parallel_invoke(
   [&count1] {
      for(int i = 0; i < 100000000; ++i)
         ++count1;
   },
   [&count2] {
      for(int i = 0; i < 100000000; ++i)
         ++count2;
   }
);
long count = count1 + count2;

Salah satu cara untuk menghilangkan berbagi palsu adalah dengan memastikan bahwa variabel penghitung berada di baris cache terpisah. Contoh berikut menyelaraskan variabel count1 dan count2 pada batas 64 byte.

__declspec(align(64)) long count1 = 0L;      
__declspec(align(64)) long count2 = 0L;      
concurrency::parallel_invoke(
   [&count1] {
      for(int i = 0; i < 100000000; ++i)
         ++count1;
   },
   [&count2] {
      for(int i = 0; i < 100000000; ++i)
         ++count2;
   }
);
long count = count1 + count2;

Contoh ini mengasumsikan bahwa ukuran cache memori adalah 64 atau lebih sedikit byte.

Kami menyarankan agar Anda menggunakan kelas konkurensi::combinable saat Anda harus berbagi data di antara tugas. Kelas combinable membuat variabel thread-local singgahan sehingga berbagi palsu lebih kecil kemungkinannya. Untuk informasi selengkapnya tentang combinable kelas, lihat Kontainer dan Objek Paralel.

[Atas]

Pastikan Variabel Valid Sepanjang Masa Pakai Tugas

Saat Anda memberikan ekspresi lambda ke grup tugas atau algoritma paralel, klausul pengambilan menentukan apakah isi ekspresi lambda mengakses variabel dalam cakupan penutup menurut nilai atau dengan referensi. Saat Anda meneruskan variabel ke ekspresi lambda dengan referensi, Anda harus menjamin bahwa masa pakai variabel tersebut bertahan hingga tugas selesai.

Pertimbangkan contoh berikut yang menentukan object kelas dan perform_action fungsi. Fungsi ini membuat object variabel dan melakukan beberapa tindakan pada variabel tersebut perform_action secara asinkron. Karena tugas tidak dijamin selesai sebelum perform_action fungsi kembali, program akan crash atau menunjukkan perilaku yang tidak ditentukan jika object variabel dihancurkan ketika tugas berjalan.

// lambda-lifetime.cpp
// compile with: /c /EHsc
#include <ppl.h>

using namespace concurrency;

// A type that performs an action.
class object
{
public:
   void action() const
   {
      // TODO: Details omitted for brevity.
   }
};

// Performs an action asynchronously.
void perform_action(task_group& tasks)
{
   // Create an object variable and perform some action on 
   // that variable asynchronously.
   object obj;
   tasks.run([&obj] {
      obj.action();
   });

   // NOTE: The object variable is destroyed here. The program
   // will crash or exhibit unspecified behavior if the task
   // is still running when this function returns.
}

Bergantung pada persyaratan aplikasi, Anda dapat menggunakan salah satu teknik berikut untuk menjamin bahwa variabel tetap valid sepanjang masa pakai setiap tugas.

Contoh berikut meneruskan object variabel menurut nilai ke tugas. Oleh karena itu, tugas beroperasi pada salinan variabelnya sendiri.

// Performs an action asynchronously.
void perform_action(task_group& tasks)
{
   // Create an object variable and perform some action on 
   // that variable asynchronously.
   object obj;
   tasks.run([obj] {
      obj.action();
   });
}

object Karena variabel diteruskan oleh nilai, setiap perubahan status yang terjadi pada variabel ini tidak muncul di salinan asli.

Contoh berikut menggunakan metode konkurensi::task_group::wait untuk memastikan bahwa tugas selesai sebelum perform_action fungsi kembali.

// Performs an action.
void perform_action(task_group& tasks)
{
   // Create an object variable and perform some action on 
   // that variable.
   object obj;
   tasks.run([&obj] {
      obj.action();
   });

   // Wait for the task to finish. 
   tasks.wait();
}

Karena tugas sekarang selesai sebelum fungsi kembali, perform_action fungsi tidak lagi berperilaku asinkron.

Contoh berikut memodifikasi perform_action fungsi untuk mengambil referensi ke object variabel. Pemanggil harus menjamin bahwa masa object pakai variabel valid hingga tugas selesai.

// Performs an action asynchronously.
void perform_action(object& obj, task_group& tasks)
{
   // Perform some action on the object variable.
   tasks.run([&obj] {
      obj.action();
   });
}

Anda juga dapat menggunakan penunjuk untuk mengontrol masa pakai objek yang Anda teruskan ke grup tugas atau algoritma paralel.

Untuk informasi selengkapnya tentang ekspresi lambda, lihat Ekspresi Lambda.

[Atas]

Baca juga

Praktik Terbaik Runtime Konkurensi
Parallel Patterns Library (PPL)
Kontainer dan Objek Paralel
Algoritma Paralel
Pembatalan di PPL
Penanganan Pengecualian
Panduan: Membuat Jaringan Pemrosesan Gambar
Cara: Menggunakan parallel_invoke untuk Menulis Rutinitas Pengurutan Paralel
Cara: Menggunakan Pembatalan untuk Memutuskan dari Perulangan Paralel
Cara: Menggunakan yang dapat dikombinasikan untuk Meningkatkan Performa
Praktik Terbaik di Pustaka Agen Asinkron
Praktik Terbaik Umum dalam Runtime Konkurensi