平行模式程式庫中的最佳作法
更新:2011 年 3 月
本文件說明平行模式程式庫 (PPL) 最有效的用法。 PPL 提供一般用途的容器、物件和演算法來執行細部平行處理原則。
如需 PPL 的詳細資訊,請參閱平行模式程式庫 (PPL)。
章節
本文件包括下列章節:
不要平行處理小型迴圈主體
在最高層級表示平行處理原則
使用 parallel_invoke 解決分而擊之問題
使用取消或例外處理來中斷平行迴圈
了解取消和例外處理對物件解構的影響
不要在平行迴圈中重複封鎖
不要在取消平行工作時執行封鎖作業
不要在平行迴圈中寫入共用資料
盡可能避免偽共用
確定變數在工作的整個存留期都維持有效
不要平行處理小型迴圈主體
相當小型迴圈主體的平行處理可能會造成相關排程額外負荷大於平行處理的好處。 請考慮下列在兩個矩陣中各加入一組項目的範例。
// 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.
}
每個平行迴圈反覆項目的工作負載太小,無法從平行處理的額外負荷中獲益。 您可以在迴圈主體中執行更多工作,或循序執行迴圈,藉以提高此迴圈的效能。
回到頁首
在最高層級表示平行處理原則
只平行處理低階程式碼時,您可以引入不會隨處理器數目增加而延展的分岔/聯結建構。 「分岔/聯結」(Fork-Join) 建構中,其中一個工作會分割為更小的平行子任務並等候這些子任務完成。 每個子任務可以遞迴分割為更多的子任務。
儘管分岔/聯結模型適合用來解決各種問題,在某些情況下,同步處理額外負荷可能會減少延展性。 例如,考慮下列會處理影像資料的循序程式碼。
// 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::parallel_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(bitmaps.begin(), bitmaps.end(), [&f](Bitmap* bmp) {
ProcessImage(bmp, f);
});
}
如果平行迴圈的每個反覆項目幾乎不執行工作,或者平行迴圈所執行的工作不平衡 (也就是有些迴圈反覆項目比其他迴圈反覆項目所需時間更長),頻繁分岔和聯結工作所需的排程額外負荷可能大於平行執行的好處。 此額外負荷會隨處理器數目增加而增加。
若要減少此範例中的排程額外負荷量,您可以先平行處理外部迴圈,然後再平行處理內部迴圈,或者使用另一個平行建構 (例如管線)。 下列範例將 ProcessImages 函式修改為使用 Concurrency::parallel_for_each 演算法,以平行處理外部迴圈。
// Processes each bitmap in the provided vector.
void ProcessImages(vector<Bitmap*> bitmaps, const function<void (DWORD&)>& f)
{
parallel_for_each(bitmaps.begin(), bitmaps.end(), [&f](Bitmap* bmp) {
ProcessImage(bmp, f);
});
}
如需使用管線平行執行影像處理的類似範例,請參閱逐步解說:建立影像處理網路。
回到頁首
使用 parallel_invoke 解決分而擊之問題
「分而擊之」(Divide-And-Conquer) 問題是一種使用遞迴將工作分割為子任務的分岔/聯結建構形式。 除了 Concurrency::task_group 和 Concurrency::structured_task_group 類別之外,您還可以使用 Concurrency::parallel_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 演算法會在呼叫端內容上執行最後一個工作序列。
如需此範例的完整版本,請參閱HOW TO:使用 parallel_invoke 來撰寫平行排序常式。 如需 parallel_invoke 演算法的詳細資訊,請參閱平行演算法。
回到頁首
使用取消或例外處理來中斷平行迴圈
PPL 提供兩種方式來取消工作群組或平行演算法所執行的平行工作。 一個方式是使用 Concurrency::task_group 和 Concurrency::structured_task_group 類別所提供的取消機制。 另一種方式是在工作的工作函式主體中擲回例外狀況。 在取消平行工作樹狀結構時,取消機制比例外處理更有效率。 「平行工作樹狀結構」(Parallel Work Tree) 是一組相關工作群組,有些工作群組會包含其他工作群組。 取消機制會以由上而下的方式取消工作群組及其子工作群組。 相反地,例外狀況處理則會由下而上執行,且必須在例外狀況往上傳播時個別取消每一個子工作群組。
當您直接處理工作群組物件時,請使用 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;
// Use parallel_for to search for the element.
// The task group enables a work function to cancel the overall
// operation when it finds the result.
structured_task_group tasks;
tasks.run_and_wait([&]
{
parallel_for(std::size_t(0), count, [&](int n) {
if (a[n] == what)
{
// Set the return value and cancel the remaining tasks.
position = n;
tasks.cancel();
}
});
});
return position;
}
因為平行演算法使用工作群組,所以其中一個平行反覆項目取消父工作群組時,整體工作就會被取消。 如需此範例的完整版本,請參閱 HOW TO:使用取消來中斷平行迴圈。
雖然比起取消機制,用例外處理來取消平行工作比較沒有效率,但有些案例會適合使用例外處理。 例如,下列方法 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(_children.begin(), _children.end(), [&](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();
}
如需此範例的完整版本,請參閱 HOW TO:使用例外狀況處理來中斷平行迴圈。
如需 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 物件,而第二個工作則會取消整體工作。 為了說明,範例使用兩個 Concurrency::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...
這個程式碼範例包含下列問題,可能導致實際行為不同於預期行為:
父工作的取消導致子工作 (對 Concurrency::parallel_invoke 的呼叫) 也會被取消。 因此,不會釋放這兩個資源。
父工作的取消導致子工作擲回內部例外狀況。 因為 Container 解構函式不會處理此例外狀況,所以例外狀況會向上傳播,而不會釋放第三個資源。
子工作所擲回的例外狀況會透過 Container 解構函式傳播。 從解構函式擲回會導致應用程式處於未定義的狀態。
我們建議您不要在工作中執行重要作業 (例如釋放資源),除非確保不會取消這些工作。 此外,也建議您不要使用會在型別解構函式中擲回的執行階段功能。
回到頁首
不要在平行迴圈中重複封鎖
封鎖作業所支配的平行迴圈 (例如 Concurrency::parallel_for 或 Concurrency::parallel_for_each) 可能會導致執行階段在短時間內建立許多執行緒。
並行執行階段會在工作完成或以合作方式封鎖或讓渡時執行額外工作。 當某個平行迴圈反覆項目封鎖時,執行階段可能會開始另一個反覆項目。 沒有可用的閒置執行緒時,執行階段會建立新的執行緒。
當平行迴圈的主體偶爾封鎖時,此機制有助於發揮整體工作的最大處理能力。 不過,許多反覆項目封鎖時,執行階段會建立許多用來執行額外工作的執行緒。 這可能會導致記憶體不足狀況或硬體資源使用率不佳。
考慮下列範例,在 parallel_for 迴圈的每個反覆項目中呼叫 Concurrency::send 函式。 因為 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 方法取消平行工作之前執行封鎖作業。
當工作執行封鎖作業時,執行階段可以在第一個工作等候資料的同時,執行其他工作。 啟用使用者模式排程 (UMS) 時,執行階段會在工作執行合作式封鎖作業或涉及核心轉換的封鎖作業時執行其他工作。 啟用一般執行緒排程 (預設) 時,執行階段只會在工作執行合作式封鎖作業時執行其他工作。 當等候的工作解除封鎖時,執行階段會重新排定此工作。 執行階段通常會先重新排定最近解除封鎖的工作,然後再重新排定比較久遠前解除封鎖的工作。 因此,執行階段可能會在封鎖作業期間排定不必要的工作,這會導致效能降低。 於是,當您在取消平行工作之前執行封鎖作業時,封鎖作業可能會延遲對 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 運算子執行可能會封鎖的堆積配置。 啟用使用者模式排程 (UMS) 時,執行階段會在封鎖作業期間執行其他工作。 啟用一般執行緒排程時,執行階段只會在工作執行合作式封鎖呼叫 (例如對 Concurrency::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;
}
回到頁首
不要在平行迴圈中寫入共用資料
並行執行階段提供許多資料結構 (例如 Concurrency::critical_section),可同步處理對共用資料的並行存取。 這些資料結構適合用於許多案例,例如當多個工作不常需要資源共用存取時。
考慮下列範例使用 Concurrency::parallel_for_each 演算法和 critical_section 物件,來計算 std::array 物件中質數的計數。 這個範例不會延展,因為每個執行緒都必須等候存取共用變數 prime_sum。
critical_section cs;
prime_sum = 0;
parallel_for_each(a.begin(), a.end(), [&](int i) {
cs.lock();
prime_sum += (is_prime(i) ? i : 0);
cs.unlock();
});
這個範例也會導致效能降低,因為頻繁的封鎖作業實際上會序列化迴圈。 此外,當並行執行階段物件執行封鎖作業時,排程器可能會在第一個執行緒等候資料時,建立其他執行緒以執行其他工作。 如果執行階段因為許多工作正在等候共用資料而建立許多執行緒,應用程式的效能可能會降低或進入資源不足的狀態。
PPL 定義了 Concurrency::combinable 類別,可藉由以無鎖定的方式提供對共用資源的存取,協助您排除共用狀態。 combinable 類別提供執行緒區域儲存區,可讓您執行細部計算,然後將這些計算合併成最終的結果。 您可以將 combinable 物件視為削減變數。
下列範例修改前述範例,改用 combinable 物件 (而不是 critical_section 物件) 來計算總和。 這個範例會延展,因為每個執行緒都會保存一份自己的區域總和。 這個範例使用 Concurrency::combinable::combine 方法,將這些區域運算合併成最終結果。
combinable<int> sum;
parallel_for_each(a.begin(), a.end(), [&](int i) {
sum.local() += (is_prime(i) ? i : 0);
});
prime_sum = sum.combine(plus<int>());
如需此範例的完整版本,請參閱 HOW TO:使用可組合的類別改善效能。 如需 combinable 類別的詳細資訊,請參閱平行容器和物件。
回到頁首
盡可能避免偽共用
當執行於個別處理器的多個並行工作寫入位於同一個快取行的變數時,會發生「偽共用」(False Sharing)。 當一個工作寫入其中一個變數時,兩個變數的快取行會失效。 每當快取行失效時,每個處理器必須重新載入此快取行。 因此,偽共用可能會導致應用程式的效能降低。
下列基本範例示範兩個並行工作,每一個都會遞增一個共用計數器變數。
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);
}
);
為了排除這兩個工作之間的資料共用,您可以將範例修改為使用兩個計數器變數。 這個範例會在工作完成後計算最終的計數器值。 不過,這個範例說明偽共用,因為 count1 和 count2 變數可能位於同一個快取行。
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;
排除偽共用的一個方式是確定計數器變數位於個別的快取行。 下列範例會將 count1 和 count2 變數對齊 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 Expressions in C++。
回到頁首
請參閱
工作
HOW TO:使用 parallel_invoke 來撰寫平行排序常式
概念
其他資源
變更記錄
日期 |
記錄 |
原因 |
---|---|---|
2011 年 3 月 |
加入如何在平行迴圈中避免重複封鎖,以及取消和例外處理對物件解構如何造成影響的資訊。 |
資訊加強。 |
2010 年 5 月 |
擴充方針。 |
資訊加強。 |