平行模式程式庫中的最佳作法

本文件說明平行模式程式庫 (PPL) 最有效的用法。 PPL 提供一般用途的容器、物件和演算法來執行細部平行處理原則。

如需 PPL 的詳細資訊,請參閱 平行模式程式庫 (PPL)

區段

本文件包含下列章節:

不要平行處理小型迴圈主體

平行處理相對小型迴圈主體時,所造成的相關排程額外負荷可能會超過平行處理的好處。 請參考下列範例,其會在兩個矩陣中各加入一組項目。

// 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.
}

每個平行迴圈反覆項目的工作負載太小,無法從平行處理的額外負荷中獲益。 您可以在迴圈主體中執行更多工作,或循序執行迴圈,藉以提高此迴圈的效能。

[靠上]

最高可能層級的 Express Parallelism

只平行處理低階程式碼時,您可以引入不會隨處理器數目增加而延展的分岔/聯結建構。 分支聯結 建構是一個建構,其中一個工作會將工作分成較小的平行子工作,並等候這些子工作完成。 每個子任務可以遞迴分割為更多的子任務。

儘管分岔/聯結模型適合用來解決各種問題,在某些情況下,同步處理的額外負荷可能會降低延展性。 例如,請參考下列處理影像資料的循序程式碼。

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

因為每個迴圈反覆項目都是獨立的,所以您可以平行處理大部分的工作,如下列範例所示。 此範例會使用 concurrency::p arallel_for 演算法平行化外部迴圈。

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

下列範例會在迴圈中呼叫 ProcessImage 函式,藉以說明分岔/聯結建構。 對 ProcessImage 的每個呼叫會等到每個子任務都已經完成之後才傳回。

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

如果平行迴圈的每個反覆項目幾乎不執行工作,或者平行迴圈所執行的工作不平衡 (也就是有些迴圈反覆項目比其他迴圈反覆項目所需時間更長),頻繁分岔和聯結工作所需的排程額外負荷可能會超過平行執行的好處。 此額外負荷會隨處理器數目增加而增加。

若要減少此範例中的排程額外負荷量,您可以先平行處理外部迴圈,然後再平行處理內部迴圈,或者使用另一個平行建構 (例如管線)。 下列範例會修改 函 ProcessImages 式,以使用 concurrency::p arallel_for_each 演算法平行化外部迴圈。

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

如需使用管線平行執行影像處理的類似範例,請參閱 逐步解說:建立影像處理網路

[靠上]

使用parallel_invoke來解決分割和征服問題

分割和征服 問題是分叉聯結建構的形式,使用遞迴將工作分成子工作。 除了 並行::task_group 平行存取::structured_task_group 類別之外,您也可以使用 concurrency::p arallel_invoke 演算法來解決分割和征服問題。 parallel_invoke 演算法的語法比工作群組物件的語法更簡潔,如果您有固定的平行工作數時,此演算法很實用。

在下列範例中,會示範如何使用 parallel_invoke 演算法來實作雙調排序演算法。

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

為了減少額外負荷,parallel_invoke 演算法會在呼叫端內容上執行最後一個工作序列。

如需此範例的完整版本,請參閱 如何:使用parallel_invoke撰寫平行排序常式 。 如需演算法 parallel_invoke 的詳細資訊,請參閱 平行演算法

[靠上]

使用取消或例外狀況處理從平行迴圈中斷

PPL 提供兩種方式來取消工作群組或平行演算法所執行的平行工作。 其中一種方式是使用並行::task_group 平行存取::structured_task_group 類別所提供的 取消機制。 另一種方式是在工作的工作函式主體中擲回例外狀況。 在取消平行工作樹狀時,取消機制比例外狀況處理更有效率。 平行工作樹狀結構 是一組相關的工作組,其中某些工作組包含其他工作組。 取消機制會以由上而下的方式取消工作群組及其子工作群組。 相反地,例外狀況處理則使用由下而上的方式執行,且必須在例外狀況往上傳播時個別取消每一個子工作群組。

當您直接使用工作組物件時,請使用 concurrency::task_group::cancel concurrency::structured_task_group::cancel 方法來取消屬於該工作組的工作。 若要取消平行演算法 (例如 parallel_for,請建立父工作群組並取消該工作群組。 例如,考慮下列函式 parallel_find_any,這個函式會在陣列中平行搜尋值。

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

因為平行演算法使用工作群組,所以其中一個平行反覆項目取消父工作群組時,整體工作就會被取消。 如需此範例的完整版本,請參閱 如何:使用取消中斷平行迴圈

雖然比起取消機制,用例外狀況處理來取消平行工作比較沒有效率,但有些案例會適合使用例外狀況處理。 例如,下列方法 for_all 會以遞迴方式在 tree 結構的每個節點上執行工作函式。 在此範例中 _children ,資料成員是 包含 tree 物件的 std::list

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

如果不需要在樹狀的每個項目上呼叫工作函式,tree::for_all 方法的呼叫端可以擲回例外狀況。 下列範例顯示 search_for_value 函式,這個函式會在提供的 tree 物件中搜尋值。 search_for_value 函式所使用的工作函式會在樹狀結構的目前項目符合所提供的值時擲回例外狀況。 search_for_value 函式會使用 try-catch 區塊來擷取例外狀況並將結果列印至主控台。

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

如需此範例的完整版本,請參閱 如何:使用例外狀況處理從平行迴圈 中斷。

如需 PPL 所提供之取消和例外狀況處理機制的詳細資訊,請參閱 PPL 例外狀況處理 中的取消。

[靠上]

瞭解取消和例外狀況處理如何影響物件解構

在平行工作的樹狀結構中,已取消的工作會導致子工作無法執行。 如果其中一項子工作執行的作業,對您的應用程式很重要,例如釋放資源,這可能會造成問題。 此外,工作取消可能會導致例外狀況透過物件解構函式傳播,而在應用程式中造成未定義的行為。

在下列範例中,Resource 類別描述資源,而 Container 類別描述保存資源的容器。 在其解構函式中,Container 類別會在其中兩個 Resource 成員上平行呼叫 cleanup 方法,然後在其第三個 Resource 成員上呼叫 cleanup 方法。

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

儘管這個模式本身沒有問題,請考慮下列平行執行兩個工作的程式碼。 第一個工作會建立 Container 物件,而第二個工作則會取消整體工作。 例如,此範例會使用兩個 並行::event 物件,以確保取消會在建立 物件之後 Container 發生,而且 Container 取消作業之後會終結物件。

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

這個範例會產生下列輸出:

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

這個程式碼範例包含下列問題,可能導致實際行為不同於預期行為:

  • 取消父工作會導致子工作、並行呼叫 ::p arallel_invoke ,也會取消。 因此,不會釋放這兩個資源。

  • 父工作的取消導致子工作擲回內部例外狀況。 因為 Container 解構函式不會處理此例外狀況,所以例外狀況會向上傳播,而不會釋放第三個資源。

  • 子工作所擲回的例外狀況會透過 Container 解構函式傳播。 從解構函式擲回會導致應用程式處於未定義的狀態。

我們建議您不要在工作中執行重要作業 (例如釋放資源),除非確定不會取消這些工作。 此外,也建議您不要使用會在類型解構函式中擲回的執行階段功能。

[靠上]

不要在平行迴圈中重複封鎖

並行迴圈,例如 concurrency::p arallel_for concurrency::p arallel_for_each ,以封鎖作業為主,可能會導致執行時間在短時間內建立許多執行緒。

並行執行階段會在工作完成或以合作方式封鎖或讓渡時執行額外工作。 當某個平行迴圈反覆項目封鎖時,執行階段可能會開始另一個反覆項目。 沒有可用的閒置執行緒時,執行階段會建立新的執行緒。

當平行迴圈的主體偶爾封鎖時,此機制有助於發揮整體工作的最大處理能力。 不過,若有許多反覆項目封鎖時,執行階段會建立許多用來執行額外工作的執行緒。 這可能會導致記憶體不足狀況或硬體資源使用率不佳。

請考慮在迴圈的每個反復專案中呼叫 concurrency::send 函式的 parallel_for 下列範例。 因為 send 會以合作方式封鎖,所以執行階段會在每次呼叫 send 時建立新執行緒以執行額外工作。

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

我們建議您重構程式碼以避免這個模式。 在這個範例中,您可以在循序的 for 迴圈中呼叫 send,藉以避免建立額外的執行緒。

[靠上]

當您取消平行工作時,請勿執行封鎖作業

可能的話,請勿在呼叫 concurrency::task_group::cancel concurrency::structured_task_group::cancel 方法來取消平行工作之前執行封鎖作業。

當工作執行合作式封鎖作業時,執行階段可以在第一個工作等候資料的同時,執行其他工作。 當等候的工作解除封鎖時,執行階段會重新排定此工作。 執行階段通常會先重新排定最近解除封鎖的工作,然後再重新排定比較久之前解除封鎖的工作。 因此,執行階段可能會在封鎖作業期間排定不必要的工作,這會導致效能降低。 於是,當您在取消平行工作之前執行封鎖作業時,封鎖作業可能會延遲對 cancel 的呼叫。 這會導致其他工作執行不必要的工作。

請參閱下列會定義 parallel_find_answer 函式的範例,這個函式會在提供的陣列中搜尋符合所提供之述詞函式的項目。 當述詞函式傳 true 回 時,平行工作函式會 Answer 建立 物件並取消整體工作。

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

new 運算子執行可能會封鎖的堆積配置。 只有當工作執行合作式封鎖呼叫時,執行時間才會執行其他工作,例如 並行呼叫::critical_section::lock

下列範例示範如何防止不必要的工作,因而改善效能。 這個範例會先取消工作群組,然後再配置儲存區給 Answer 物件。

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

[靠上]

不要在平行迴圈中寫入共用資料

並行執行時間提供數個資料結構,例如 平行存取::critical_section ,可同步處理共用資料的平行存取。 這些資料結構適合用於許多案例,例如當多個工作不常需要資源共用存取時。

請考慮使用 concurrency::p arallel_for_each 演算法和 critical_section 物件來計算 std::array 物件中 質數計數的下列範例 這個範例不會延展,因為每個執行緒都必須等候存取共用變數 prime_sum

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

這個範例也會導致效能降低,因為頻繁的封鎖作業實際上會序列化迴圈。 此外,當並行執行階段物件執行封鎖作業時,排程器可能會在第一個執行緒等候資料時,建立其他執行緒以執行其他工作。 如果執行階段因為許多工作正在等候共用資料而建立許多執行緒,應用程式的效能可能會降低或進入資源不足的狀態。

PPL 會 定義並行::combinable 類別,以無鎖定方式提供共用資源的存取權,以協助您消除共用狀態。 combinable 類別提供執行緒區域儲存區,可讓您執行細部運算,然後將這些運算合併成最終的結果。 您可以將 combinable 物件視為削減變數。

下列範例修改前述範例,改用 combinable 物件 (而不是 critical_section 物件) 來計算總和。 這個範例會延展,因為每個執行緒都會保存一份自己的區域總和。 此範例會使用 concurrency::combinable::combine 方法,將本機計算合併至最終結果。

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

如需此範例的完整版本,請參閱 如何:使用可結合以改善效能 。 如需 類別 combinable 的詳細資訊,請參閱 平行容器和物件

[靠上]

可能的話,請避免誤共用

當在個別處理器上執行的多個並行工作寫入位於相同快取行的變數時,就會發生 False 共用 。 當一個工作寫入其中一個變數時,兩個變數的快取行皆會失效。 每當快取行失效時,每個處理器必須重新載入此快取行。 因此,偽共用可能會導致應用程式的效能降低。

下列基本範例示範兩個並行工作,每一個都會遞增一個共用計數器變數。

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

為了排除這兩個工作之間的資料共用,您可以將範例修改為使用兩個計數器變數。 這個範例會在工作完成後計算最終的計數器值。 不過,這個範例說明偽共用,因為 count1count2 變數可能位於同一個快取行。

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;

排除偽共用的一個方式是確定計數器變數位於個別的快取行。 下列範例會將 count1count2 變數對齊 64 位元組界限。

__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;

這個範例假設記憶體快取大小為 64 位元組或以下。

當您必須在工作之間共用資料時,建議您使用 concurrency::combinable 類別。 combinable 類別建立執行緒區域變數的方式,使得偽共用比較不可能發生。 如需 類別 combinable 的詳細資訊,請參閱 平行容器和物件

[靠上]

請確定變數在工作的存留期內有效

當您將 Lambda 運算式提供給工作群組或平行演算法時,擷取子句指定 Lambda 運算式主體以傳值方式或傳址方式存取封閉範圍中的變數。 當您以傳址方式將變數傳遞給 lambda 運算式時,必須保證該變數的存留期會一直持續到工作完成。

請參閱下列會定義 object 類別和 perform_action 函式的範例。 perform_action 函式會建立 object 變數,然後以非同步方式在該變數上執行某項動作。 因為工作不保證會在 perform_action 函式傳回前完成,所以在工作執行的同時,如果 object 變數終結,程式會損毀或表現未指定的行為。

// 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.
}

根據應用程式需求,您可以使用下列其中一個技巧,確保變數在每個工作的整個存留期都維持有效。

下列範例會以傳值方式將 object 變數傳遞給工作。 因此,工作會在自己的變數複本上操作。

// 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 變數是以傳值方式傳遞,對此變數的任何狀態變更都不會出現在原始複本中。

下列範例會使用 concurrency::task_group::wait 方法,確定工作在函式傳回之前 perform_action 完成。

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

因為工作現在在函式傳回前完成,所以 perform_action 函式不再以非同步方式表現。

下列範例將 perform_action 函式修改為可參考 object 變數。 呼叫端必須保證 object 變數的存留期在工作完成前維持有效。

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

您也可以使用指標來控制傳遞給工作群組或平行演算法之物件的存留期。

如需 Lambda 運算式的詳細資訊,請參閱 Lambda 運算式

[靠上]

另請參閱

並行執行階段最佳做法
平行模式程式庫 (PPL)
平行容器和物件
平行演算法
PPL 中的取消
例外狀況處理
逐步解說:建立影像處理網路
如何:使用 parallel_invoke 撰寫平行排序常式
如何:使用取消來中斷平行迴圈
如何:使用 combinable 改善效能
非同步代理程式程式庫中的最佳做法
並行執行階段中的一般最佳做法