Praktik Terbaik di Perpustakaan Pola Paralel

Dokumen ini menjelaskan cara terbaik untuk memanfaatkan Perpustakaan Pola Paralel (PPL) secara efektif. PPL menyediakan wadah tujuan umum, objek, dan algoritma untuk melakukan paralelisme berbutir halus.

Untuk informasi selengkapnya tentang PPL, lihat Parallel Patterns Library (PPL).

Bagian

Dokumen ini berisi bagian-bagian berikut:

Jangan Sejajarkan Tubuh Loop Kecil

Paralelisasi badan loop yang relatif kecil dapat menyebabkan penjadwalan overhead terkait lebih besar daripada 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 loop paralel terlalu kecil untuk mendapatkan keuntungan dari overhead untuk pemrosesan paralel. Anda dapat meningkatkan kinerja loop ini dengan melakukan lebih banyak pekerjaan di badan loop atau dengan melakukan loop secara serial.

[Atas]

Ekspresikan Paralelisme pada Level Setinggi Mungkin

Ketika Anda sejajarkan kode hanya pada tingkat rendah, Anda dapat memperkenalkan konstruksi fork-join yang tidak skala sebagai 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 secara rekursif membagi dirinya 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 loop bersifat independen, Anda dapat sejajar dengan banyak pekerjaan, seperti yang ditunjukkan pada contoh berikut. Contoh ini menggunakan konkurensi::p arallel_for algoritma untuk paralelisasi loop 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 menggambarkan konstruksi fork-join dengan memanggil ProcessImage fungsi dalam loop. Setiap panggilan untuk ProcessImage tidak kembali sampai 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 iterasi loop paralel hampir tidak melakukan pekerjaan, atau pekerjaan yang dilakukan oleh loop paralel tidak seimbang, yaitu, beberapa iterasi loop memakan waktu lebih lama dari yang lain, penjadwalan overhead yang diperlukan untuk sering bercabang dan bergabung bekerja dapat lebih besar daripada manfaat untuk eksekusi paralel. Overhead ini meningkat karena jumlah prosesor meningkat.

Untuk mengurangi jumlah penjadwalan overhead dalam contoh ini, Anda dapat sejajarkan loop luar sebelum Anda sejajarkan loop dalam atau menggunakan konstruksi paralel lain seperti pipelining. Contoh berikut memodifikasi ProcessImages fungsi untuk menggunakan algoritma konkurensi::p arallel_for_each untuk paralelisasi loop 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 Image-Processing.

[Atas]

Gunakan parallel_invoke untuk Memecahkan Masalah Divide-and-Conquer

Masalah bagi-dan-menaklukkan adalah bentuk konstruksi fork-join yang menggunakan rekursi untuk memecah tugas menjadi subtugas. Selain kelas konkurensi::task_group dan konkurensi::structured_task_group , Anda juga dapat menggunakan algoritma konkurensi::p arallel_invoke untuk memecahkan masalah bagi dan menaklukkan. Algoritma ini parallel_invoke memiliki sintaks yang lebih ringkas daripada objek gugus tugas, dan berguna ketika Anda memiliki sejumlah tugas paralel yang tetap.

Contoh berikut menggambarkan penggunaan parallel_invoke algoritma untuk mengimplementasikan algoritma penyortiran bitonic.

// 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 yang terakhir dari serangkaian tugas pada konteks panggilan.

Untuk versi lengkap dari contoh ini, lihat Cara: Gunakan parallel_invoke Menulis Rutinitas Pengurutan Paralel. Untuk informasi selengkapnya tentang parallel_invoke algoritme, lihat Algoritma Paralel.

[Atas]

Gunakan Penanganan Pembatalan atau Pengecualian untuk Melepaskan diri dari Loop Paralel

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

Saat Anda bekerja langsung dengan objek grup tugas, gunakan metode konkurensi::task_group::cancel atau konkurensi::structured_task_group::cancel metode untuk membatalkan pekerjaan yang termasuk dalam grup tugas tersebut. Untuk membatalkan algoritme 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 iterasi paralel membatalkan grup tugas induk, tugas keseluruhan dibatalkan. Untuk versi lengkap dari contoh ini, lihat Cara: Gunakan Pembatalan untuk Memisahkan dari Loop 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 memberikan 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 memberikan pengecualian ketika elemen pohon saat ini cocok dengan nilai yang diberikan. 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 dari contoh ini, lihat Cara: Gunakan Penanganan Pengecualian untuk Memisahkan dari Loop Paralel.

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

[Atas]

Memahami bagaimana Penanganan Pembatalan dan Pengecualian Mempengaruhi Penghancuran Objek

Dalam pohon pekerjaan paralel, tugas yang dibatalkan mencegah tugas anak berjalan. Hal 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 untuk disebarkan melalui perusakan 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 destructornya, 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. Sebagai 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 berperilaku berbeda dari yang Anda harapkan:

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

  • Pembatalan tugas orang tua menyebabkan tugas anak untuk melemparkan pengecualian internal. Container Karena destructor tidak menangani pengecualian ini, pengecualian diperbanyak ke atas dan sumber daya ketiga tidak dibebaskan.

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

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

[Atas]

Jangan Memblokir Berulang Kali dalam Loop Paralel

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

Concurrency Runtime melakukan pekerjaan tambahan ketika tugas selesai atau diblokir atau dihasilkan secara kooperatif. Ketika satu iterasi loop paralel diblokir, runtime mungkin memulai iterasi lain. Ketika tidak ada thread idle yang tersedia, runtime membuat thread baru.

Ketika tubuh loop paralel kadang-kadang blok, mekanisme ini membantu memaksimalkan keseluruhan throughput tugas. Namun, ketika banyak iterasi diblokir, runtime dapat membuat banyak utas untuk menjalankan pekerjaan tambahan. Hal ini dapat menyebabkan kondisi memori rendah atau pemanfaatan sumber daya perangkat keras yang buruk.

Pertimbangkan contoh berikut yang memanggil fungsi konkurensi::kirim di setiap iterasi parallel_for loop. Karena send blok 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 Anda refactor kode Anda untuk menghindari pola ini. Dalam contoh ini, Anda dapat menghindari pembuatan utas tambahan dengan memanggil send dalam loop serial for .

[Atas]

Jangan Lakukan Operasi Pemblokiran Saat Anda Membatalkan Pekerjaan Paralel

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

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

Pertimbangkan contoh berikut yang mendefinisikan parallel_find_answer fungsi, yang mencari elemen dari array yang disediakan yang memenuhi fungsi predikat yang disediakan. Ketika fungsi predikat kembali true, fungsi kerja paralel membuat objek Answer dan membatalkan tugas secara 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 heap, yang mungkin diblokir. 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 kinerja. 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 Loop Paralel

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

Pertimbangkan contoh berikut yang menggunakan konkurensi::p arallel_for_each algoritma dan critical_section objek untuk menghitung jumlah bilangan prima 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 kinerja yang buruk karena operasi penguncian yang sering secara efektif membuat serial loop. Selain itu, ketika objek Concurrency Runtime melakukan operasi pemblokiran, penjadwal mungkin membuat utas tambahan untuk melakukan pekerjaan lain sementara utas pertama menunggu data. Jika runtime membuat banyak utas karena banyak tugas menunggu data bersama, aplikasi dapat berkinerja buruk atau memasuki 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 thread-local yang memungkinkan Anda melakukan perhitungan berbutir halus dan kemudian menggabungkan perhitungan tersebut menjadi hasil akhir. Anda dapat menganggap objek combinable sebagai variabel reduksi.

Contoh berikut memodifikasi yang sebelumnya dengan menggunakan objek combinable , bukan critical_section objek untuk menghitung jumlahnya. Contoh ini diskalakan karena setiap utas menyimpan salinan jumlah lokalnya sendiri. Contoh ini menggunakan metode konkurensi::combinable::combine untuk menggabungkan perhitungan 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 dari contoh ini, lihat Cara: Gunakan gabungan untuk Meningkatkan Kinerja. 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 pada 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 kinerja dalam aplikasi Anda.

Contoh dasar berikut menunjukkan dua tugas bersamaan yang masing-masing meningkatkan 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 kedua 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 cenderung terletak pada 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 memastikan bahwa variabel penghitung berada pada garis 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.

Sebaiknya gunakan concurrency::combinable class saat Anda harus berbagi data di antara tugas. Kelas combinable membuat variabel thread-local sedemikian rupa sehingga berbagi palsu lebih kecil kemungkinannya. Untuk informasi selengkapnya tentang combinable kelas, lihat Kontainer dan Objek Paralel.

[Atas]

Pastikan Bahwa Variabel Valid Sepanjang Masa Tugas

Saat Anda memberikan ekspresi lambda ke grup tugas atau algoritme paralel, klausa pengambilan menentukan apakah badan ekspresi lambda mengakses variabel dalam cakupan penutup berdasarkan nilai atau dengan referensi. Saat Anda meneruskan variabel ke ekspresi lambda dengan referensi, Anda harus menjamin bahwa masa pakai variabel tersebut berlanjut hingga tugas selesai.

Pertimbangkan contoh berikut yang mendefinisikan object kelas dan perform_action fungsi. Fungsi ini perform_action membuat object variabel dan melakukan beberapa tindakan pada variabel tersebut 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.
}

Tergantung pada persyaratan aplikasi Anda, Anda dapat menggunakan salah satu teknik berikut untuk memastikan bahwa variabel tetap berlaku sepanjang masa tugas.

Contoh berikut meneruskan object variabel berdasarkan 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 dilewatkan oleh nilai, setiap perubahan keadaan yang terjadi pada variabel ini tidak muncul dalam salinan asli.

Contoh berikut menggunakan metode konkurensi::task_group::tunggu 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. Penelepon harus menjamin bahwa masa pakai object variabel valid sampai 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 lewati ke grup tugas atau algoritme paralel.

Untuk informasi selengkapnya tentang ekspresi lambda, lihat Ekspresi Lambda.

[Atas]

Lihat juga

Praktik Terbaik Runtime Konkurensi
Pustaka Pola Paralel (PPL)
Kontainer dan Objek Paralel
Algoritma Paralel
Pembatalan di PPL
Penanganan Pengecualian
Panduan: Membuat Jaringan Image-Processing
Cara: Gunakan parallel_invoke untuk Menulis Rutinitas SortOr Paralel
Cara: Gunakan Pembatalan untuk Melepaskan diri dari Loop Paralel
Cara: Gunakan gabungan untuk Meningkatkan Kinerja
Praktik Terbaik di Perpustakaan Agen Asinkron
Praktik Terbaik Umum dalam Runtime Konkurensi