PPL簡介
并行模式庫 (PPL) 提供命令式編程模型,以促進(jìn)開發(fā)并發(fā)應(yīng)用程序的可擴(kuò)展性和易用性扇苞。 PPL 構(gòu)建在并發(fā)運(yùn)行時(shí)的計(jì)劃和資源管理組件上欺殿。
通過提供并行作用于數(shù)據(jù)的泛型安全算法和容器,提高應(yīng)用程序代碼與基礎(chǔ)線程機(jī)制之間的抽象級別鳖敷。 使用 PPL 還可以開發(fā)通過為共享狀態(tài)提供替代方案實(shí)現(xiàn)縮放的應(yīng)用程序脖苏。
PPL 提供以下功能:
- 任務(wù)并行︰ 一種工作在Windows線程池上,可以并行地執(zhí)行多個(gè)任務(wù)的機(jī)制
- 并行算法︰ 利用并發(fā)運(yùn)行時(shí)(Concurrency Runtime )定踱,并行處理數(shù)據(jù)集合的泛型算法
- 并行容器和對象︰ 提供對其元素的安全并發(fā)訪問的泛型容器類型
并行算法
并行模式庫 (PPL) 提供了在數(shù)據(jù)集合并發(fā)執(zhí)行工作的算法棍潘。 這些算法類似于標(biāo)準(zhǔn)模板庫 (STL)所提供的。
并行算法由并發(fā)運(yùn)行時(shí)中的現(xiàn)有功能組成。 例如, concurrency:: parallel_for 算法使用 concurrency:: structured_task_group 對象執(zhí)行并行循環(huán)迭代飞袋。 parallel_for
算法以最佳方式工作分區(qū)給定可用數(shù)量的計(jì)算資源震叮。
parallel_for 算法
簡介
Concurrency:: parallel_for 算法重復(fù)地以并行方式執(zhí)行相同的任務(wù)汹胃。 每個(gè)任務(wù)是由迭代值參數(shù)化。 當(dāng)您具有不共享資源分布該循環(huán)的迭代循環(huán)主體時(shí),此算法很有用。
parallel_for
算法以可最佳并行執(zhí)行的方式對任務(wù)進(jìn)行分區(qū)赛蔫。 當(dāng)工作負(fù)載不平衡時(shí),此算法還會(huì)使用工作竊取算法和范圍竊取來平衡這些分區(qū)泥张。 當(dāng)某個(gè)循環(huán)迭代以協(xié)作方式被阻塞時(shí)呵恢,則運(yùn)行時(shí)將原本分配當(dāng)前線程的迭代范圍重新分配給其他線程或處理器。 同樣媚创,當(dāng)某個(gè)線程完成迭代范圍渗钉,則運(yùn)行時(shí)將原本屬于其他線程的工作重新分配給該線程。 parallel_for
算法還支持 嵌套并行钞钙。 當(dāng)一個(gè)并行循環(huán)中包含另一個(gè)并行循環(huán)時(shí)晌姚,運(yùn)行時(shí)之間以高效并行執(zhí)行的方式協(xié)調(diào)處理循環(huán)主體的資源粤剧。
parallel_for
算法有多個(gè)重載版本。 第一個(gè)版本采用起始值挥唠、 結(jié)束值和工作函數(shù) (lambda 表達(dá)式、 函數(shù)對象或函數(shù)指針)焕议。 第二個(gè)版本所依據(jù)采用起始值宝磨、 結(jié)束值、 一個(gè)步長值和工作函數(shù)盅安。 此函數(shù)的第一個(gè)版本使用 1 作為步長值唤锉。 其余版本采用分區(qū)程序?qū)ο螅鼓軌蛑付?parallel_for
如何在線程之間對范圍進(jìn)行分區(qū)别瞭。 分區(qū)程序在 分區(qū)工作 中有更詳細(xì)地介紹窿祥。
您可以將許多 for
循環(huán),轉(zhuǎn)換成 parallel_for
蝙寨。 但是晒衩, parallel_for
算法與 for
語句以下不同點(diǎn)︰
parallel_for
算法不再以預(yù)先確定的順序執(zhí)行這些任務(wù)。parallel_for
算法不支持任意終止條件墙歪。parallel_for
當(dāng)?shù)兞康漠?dāng)前值小于上一次時(shí)听系,并行算法就停止了。_Index_type
類型參數(shù)必須是整數(shù)類型虹菲。 此整型可以有符號(hào)或無符號(hào)靠胜。循環(huán)迭代必須前滾。 如果
_Step
參數(shù)是小于 1毕源,parallel_for
算法將引發(fā)類型的異常 std::invalid_argument 浪漠。parallel_for
算法的異常處理機(jī)制不同于for
循環(huán)。 如果在并行循環(huán)主體中同時(shí)發(fā)生多個(gè)異常霎褐,運(yùn)行時(shí)僅會(huì)將一個(gè)異常傳播到調(diào)用parallel_for
的線程址愿。 此外,當(dāng)某個(gè)循環(huán)迭代拋出異常瘩欺,則運(yùn)行時(shí)不會(huì)立即停止整個(gè)循環(huán)必盖。 相反,該循環(huán)處于已取消狀態(tài)俱饿,并且運(yùn)行時(shí)會(huì)丟棄尚未啟動(dòng)的任何任務(wù)歌粥。 有關(guān)異常處理和并行算法的詳細(xì)信息,請參閱 異常處理拍埠。
盡管 parallel_for
算法不支持任意終止條件失驶,但你可以使用取消以停止所有的任務(wù)。 有關(guān)取消的詳細(xì)信息枣购,請參閱 取消嬉探。
注意
對負(fù)載均衡和取消之類的功能的支持擦耀,可能會(huì)抵消并行執(zhí)行循環(huán)主體可能帶來的好處,尤其當(dāng)循環(huán)體相對較小時(shí)涩堤。
您可以在并行循環(huán)中使用分區(qū)程序來盡量減少此開銷眷蜓。 有關(guān)詳細(xì)信息,請參閱 分區(qū)工作 胎围。
示例
下面的示例演示的基本結(jié)構(gòu) parallel_for 算法吁系。 本示例向控制臺(tái)打印范圍 [1,5] 并行中的每個(gè)值白魂。
// parallel-for-structure.cpp
// compile with: /EHsc
#include <ppl.h>
#include <array>
#include <sstream>
#include <iostream>
using namespace concurrency;
using namespace std;
int wmain()
{
// Print each value from 1 to 5 in parallel.
parallel_for(1, 6, [](int value) {
wstringstream ss;
ss << value << L' ';
wcout << ss.str();
});
}
該示例產(chǎn)生下面的示例輸出︰
1 2 4 3 5
注意
因?yàn)?parallel_for
算法作用于并行每個(gè)項(xiàng)目汽纤,向控制臺(tái)打印值的順序?qū)?huì)不同。
有關(guān)完整的示例使用 parallel_for
算法福荸,請參閱 如何︰ 編寫 parallel_for 循環(huán)蕴坪。
parallel_for_each 算法
Concurrency:: parallel_for_each 算法對迭代容器(如 STL提供的)并行執(zhí)行任務(wù)。 它跟parallel_for
算法一樣使用相同的分區(qū)邏輯敬锐。
parallel_for_each
算法類似于 STL for_each 算法背传,不同之處在于 parallel_for_each
算法并發(fā)執(zhí)行任務(wù)。 像其他并行算法一樣滞造, parallel_for_each
不會(huì)按特定順序執(zhí)行這些任務(wù)续室。
盡管 parallel_for_each
算法支持前向迭代器和隨機(jī)訪問迭代器,但隨機(jī)訪問迭代器表現(xiàn)更好谒养。
示例
下面的示例演示parallel_for_each
算法的基本結(jié)構(gòu)挺狰。 此示例并行地將 std:: array對象中的每個(gè)值打印到控制臺(tái) 。
// parallel-for-each-structure.cpp
// compile with: /EHsc
#include <ppl.h>
#include <array>
#include <sstream>
#include <iostream>
using namespace concurrency;
using namespace std;
int wmain()
{
// Create an array of integer values.
array<int, 5> values = { 1, 2, 3, 4, 5 };
// Print each value in the array in parallel.
parallel_for_each(begin(values), end(values), [](int value) {
wstringstream ss;
ss << value << L' ';
wcout << ss.str();
});
}
/* Sample output:
5 4 3 1 2
*/
輸出如下:
4 5 1 2 3
注意
因?yàn)?parallel_for_each
算法并行作用于每個(gè)元素买窟,向控制臺(tái)打印值的順序?qū)?huì)不同丰泊。
有關(guān)完整的示例使用 parallel_for_each
算法,請參閱 如何︰ 編寫 parallel_for_each 循環(huán)始绍。
parallel_invoke 算法
Concurrency:: parallel_invoke 算法以并行方式執(zhí)行一組任務(wù)瞳购。 每個(gè)任務(wù)完成之前不返回。 當(dāng)您想要在同一時(shí)間執(zhí)行多個(gè)獨(dú)立的任務(wù)時(shí)亏推,此算法很有用学赛。
parallel_invoke
算法將一系列工作函數(shù)作為其參數(shù) (lambda 函數(shù)、 函數(shù)對象或函數(shù)指針)吞杭。 parallel_invoke
算法被重載成以 2 到 10 個(gè)參數(shù)之間的形式盏浇。 傳遞給 parallel_invoke
的每個(gè)函數(shù)必須都必須是不帶參的。
像其他并行算法 parallel_invoke
不會(huì)按特定順序執(zhí)行這些任務(wù)芽狗。 任務(wù)并行 解釋了parallel_invoke
算法如何關(guān)聯(lián)不同的任務(wù)和任務(wù)組绢掰。
示例
下面的示例演示 parallel_invoke
算法的基本結(jié)構(gòu)。 此示例用三個(gè)局部變量并發(fā)調(diào)用 twice
函數(shù),并輸出結(jié)果到控制臺(tái)滴劲。
// parallel-invoke-structure.cpp
// compile with: /EHsc
#include <ppl.h>
#include <string>
#include <iostream>
using namespace concurrency;
using namespace std;
// Returns the result of adding a value to itself.
template <typename T>
T twice(const T& t) {
return t + t;
}
int wmain()
{
// Define several values.
int n = 54;
double d = 5.6;
wstring s = L"Hello";
// Call the twice function on each value concurrently.
parallel_invoke(
[&n] { n = twice(n); },
[&d] { d = twice(d); },
[&s] { s = twice(s); }
);
// Print the values to the console.
wcout << n << L' ' << d << L' ' << s << endl;
}
輸出:
108 11.2 HelloHello
有關(guān)使用 parallel_invoke
算法的完整示例攻晒,請參閱 如何︰ 使用 parallel_invoke 來編寫并行排序例程 和 如何︰ 使用 parallel_invoke 來執(zhí)行并行操作。
parallel_transform 和 parallel_reduce 算法
Concurrency:: parallel_transform 和 concurrency:: parallel_reduce 算法分別是 STL 算法 std:: transform 和 std:: accumulate的并行版本班挖。 并發(fā)運(yùn)行時(shí)版本的行為類似于 STL 版本鲁捏,只不過因?yàn)樗鼈兪遣⑿袌?zhí)行的,所以操作順序不確定聪姿。 當(dāng)您使用的集合足夠大碴萧,可從并行處理中獲得性能和可擴(kuò)展性優(yōu)勢時(shí),請使用這些算法末购。
注意
因?yàn)檫@些迭代器會(huì)生成穩(wěn)定的內(nèi)存地址,所以 parallel_transform 算法和 parallel_reduce 算法僅支持隨機(jī)訪問虎谢、雙向和向前迭代器盟榴。
而且這些迭代器必須生成非 const 左值。
parallel_transform 算法
您可以使用 parallel transform
算法執(zhí)行許多數(shù)據(jù)并行化操作婴噩。 例如擎场,你可以:
調(diào)整圖像的亮度,并執(zhí)行其他圖像處理操作几莽。
在兩個(gè)向量之間求和或取點(diǎn)積迅办,并對向量執(zhí)行其他數(shù)值計(jì)算。
執(zhí)行三維射線跟蹤章蚣,其中每次迭代引用一個(gè)必須呈現(xiàn)的像素站欺。
下面的示例顯示用于調(diào)用 parallel_transform
算法的基本結(jié)構(gòu)。 此示例中采用兩種方式對std::vector對象中的每個(gè)元素求反纤垂。 第一種方法是使用 lambda 表達(dá)式矾策。 第二種方法是使用 std::negate, ,它派生自 std::unary_function峭沦。
// basic-parallel-transform.cpp
// compile with: /EHsc
#include <ppl.h>
#include <random>
using namespace concurrency;
using namespace std;
int wmain()
{
// Create a large vector that contains random integer data.
vector<int> values(1250000);
generate(begin(values), end(values), mt19937(42));
// Create a vector to hold the results.
// Depending on your requirements, you can also transform the
// vector in-place.
vector<int> results(values.size());
// Negate each element in parallel.
parallel_transform(begin(values), end(values), begin(results), [](int n) {
return -n;
});
// Alternatively, use the negate class to perform the operation.
parallel_transform(begin(values), end(values), begin(results), negate<int>());
}
注意
本示例演示 parallel_transform 的基本用法贾虽。 由于工作函數(shù)不會(huì)執(zhí)行大量工作,因此本示例中不會(huì)有顯著的性能提升吼鱼。
parallel_transform
算法有兩個(gè)重載蓬豁。 第一個(gè)重載采用一個(gè)輸入范圍和一個(gè)一元函數(shù)。 該一元函數(shù)可以是采用一個(gè)參數(shù)的 lambda 表達(dá)式菇肃、一個(gè)函數(shù)對象或從 unary_function
派生的一個(gè)類型地粪。 第二個(gè)重載采用兩個(gè)輸入范圍和一個(gè)二元函數(shù)。 該二元函數(shù)可以采用兩個(gè)參數(shù)巷送、 函數(shù)對象或派生自的類型的 lambda 表達(dá)式 std:: binary_function驶忌。 下面的示例闡釋了這些差異。
//
// Demonstrate use of parallel_transform together with a unary function.
// This example uses a lambda expression.
parallel_transform(begin(values), end(values),
begin(results), [](int n) {
return -n;
});
// Alternatively, use the negate class:
parallel_transform(begin(values), end(values),
begin(results), negate<int>());
//
// Demonstrate use of parallel_transform together with a binary function.
// This example uses a lambda expression.
parallel_transform(begin(values), end(values), begin(results),
begin(results), [](int n, int m) {
return n * m;
});
// Alternatively, use the multiplies class:
parallel_transform(begin(values), end(values), begin(results),
begin(results), multiplies<int>());
注意
您為 parallel_transform 的輸出提供的迭代器必須與輸入迭代器完全重疊或根本不重疊。 如果輸入迭代器和輸出迭代器部分重疊付魔,則此算法的行為是未指定的聊品。
parallel_reduce 算法
當(dāng)您具有滿足關(guān)聯(lián)屬性的操作序列時(shí),parallel_reduce 算法很有用几苍。 (此算法不要求可交換屬性翻屈。)以下是可以使用 parallel_reduce 執(zhí)行的一些操作:
將矩陣的序列相乘以生成一個(gè)矩陣。
用矩陣序列乘以一個(gè)向量來生成一個(gè)向量妻坝。
計(jì)算字符串序列的長度伸眶。
將一個(gè)元素列表(例如字符串)組合為一個(gè)元素。
下面的基本示例演示如何使用 parallel_reduce 算法將一個(gè)字符串序列組合為一個(gè)字符串刽宪。 與 parallel_transform 的示例一樣厘贼,此基本示例中不會(huì)有性能提升。
// basic-parallel-reduce.cpp
// compile with: /EHsc
#include <ppl.h>
#include <iostream>
#include <string>
#include <vector>
using namespace concurrency;
using namespace std;
int wmain()
{
// Create a vector of strings.
vector<wstring> words;
words.push_back(L"Lorem ");
words.push_back(L"ipsum ");
words.push_back(L"dolor ");
words.push_back(L"sit ");
words.push_back(L"amet, ");
words.push_back(L"consectetur ");
words.push_back(L"adipiscing ");
words.push_back(L"elit.");
// Reduce the vector to one string in parallel.
wcout << parallel_reduce(begin(words), end(words), wstring()) << endl;
}
/* Output:
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
*/
在許多情況下圣拄,您可以將 parallel_reduce
當(dāng)成是parallel_for_each
和 concurrency:: combinable 類一起使用的的簡寫形式嘴秸。
示例︰ 并行執(zhí)行映射和歸約
一個(gè) 映射 操作將函數(shù)應(yīng)用于序列中的每個(gè)值。 一個(gè) 歸約 操作會(huì)將序列中的每一個(gè)元素組合成一個(gè)值庇谆。 可以使用標(biāo)準(zhǔn)模板庫 (STL) std:: transform和std:: accumulate 類來執(zhí)行映射和歸約操作岳掐。 但是,對于許多問題饭耳,您可以使用 parallel_transform
算法并行執(zhí)行映射操作串述,并使用 parallel_reduce
算法并行執(zhí)行歸約操作。
下面的示例將按串行方式計(jì)算質(zhì)數(shù)和所需的時(shí)間與按并行方式計(jì)算質(zhì)數(shù)和所需的時(shí)間進(jìn)行比較寞肖。 映射階段會(huì)將非質(zhì)數(shù)值轉(zhuǎn)換為 0纲酗,而歸約階段將對這些值求和。
// parallel-map-reduce-sum-of-primes.cpp
// compile with: /EHsc
#include <windows.h>
#include <ppl.h>
#include <array>
#include <numeric>
#include <iostream>
using namespace concurrency;
using namespace std;
// Calls the provided work function and returns the number of milliseconds
// that it takes to call that function.
template <class Function>
__int64 time_call(Function&& f)
{
__int64 begin = GetTickCount();
f();
return GetTickCount() - begin;
}
// Determines whether the input value is prime.
bool is_prime(int n)
{
if (n < 2)
return false;
for (int i = 2; i < n; ++i)
{
if ((n % i) == 0)
return false;
}
return true;
}
int wmain()
{
// Create an array object that contains 200000 integers.
array<int, 200000> a;
// Initialize the array such that a[i] == i.
iota(begin(a), end(a), 0);
int prime_sum;
__int64 elapsed;
// Compute the sum of the numbers in the array that are prime.
elapsed = time_call([&] {
transform(begin(a), end(a), begin(a), [](int i) {
return is_prime(i) ? i : 0;
});
prime_sum = accumulate(begin(a), end(a), 0);
});
wcout << prime_sum << endl;
wcout << L"serial time: " << elapsed << L" ms" << endl << endl;
// Now perform the same task in parallel.
elapsed = time_call([&] {
parallel_transform(begin(a), end(a), begin(a), [](int i) {
return is_prime(i) ? i : 0;
});
prime_sum = parallel_reduce(begin(a), end(a), 0);
});
wcout << prime_sum << endl;
wcout << L"parallel time: " << elapsed << L" ms" << endl << endl;
}
/* Sample output:
1709600813
serial time: 7406 ms
1709600813
parallel time: 1969 ms
*/
注意
有關(guān)執(zhí)行映射和化簡操作的并行的另一個(gè)示例逝淹,請參閱 如何︰ 并行執(zhí)行映射和歸約操作耕姊。
分區(qū)工作
若要在數(shù)據(jù)源上并行化一個(gè)操作,不可或缺的步驟為將操作源 分區(qū)成可被多線程并發(fā)操作的幾塊栅葡。 分區(qū)程序將指定并行算法如何在線程間劃分區(qū)間范圍茉兰。 如本文檔前面所述,PPL 使用的是默認(rèn)分區(qū)機(jī)制欣簇,該默認(rèn)分區(qū)機(jī)制創(chuàng)建初始工作負(fù)荷并在工作負(fù)荷不平衡時(shí)使用工作竊取算法和范圍竊取來平衡這些分區(qū)规脸。 例如,當(dāng)某個(gè)循環(huán)迭代完成一個(gè)迭代范圍時(shí)熊咽,運(yùn)行時(shí)會(huì)將其他線程的工作重新分配給該線程莫鸭。 但是,在某些方案中横殴,您可能希望指定另一個(gè)更適用于您的問題的分區(qū)機(jī)制被因。
parallel_for
卿拴、parallel_for_each
和 parallel_transform
算法提供有一附加參數(shù) _Partitioner
的重載版本。 此參數(shù)定義了用于劃分工作的分區(qū)程序類型梨与。 以下是 PPL 定義的分區(qū)程序種類:
concurrency::affinity_partitioner
將工作劃分為一個(gè)固定數(shù)量的范圍(通常是可用于在循環(huán)中工作的輔助線程的數(shù)量)堕花。 此分區(qū)程序類型與 static_partitioner
類似,但通過將范圍映射到輔助線程的方式改善了緩存的關(guān)聯(lián)粥鞋。 當(dāng)在相同數(shù)據(jù)集中多次執(zhí)行一個(gè)循環(huán)(例如一個(gè)循環(huán)內(nèi)的循環(huán))且數(shù)據(jù)適合緩存時(shí)缘挽,此分區(qū)程序類型可提高性能。 此分區(qū)程序不完全適用取消呻粹。 它也不使用協(xié)作阻塞語義壕曼,因此不能與具有前向依賴關(guān)系的并行循環(huán)一起使用。
concurrency::auto_partitioner
將工作劃分為一個(gè)初始數(shù)量的范圍(通常是可用于在循環(huán)中工作的輔助線程的數(shù)量)等浊。 當(dāng)您不調(diào)用采用 _Partitioner
參數(shù)的重載的并行算法時(shí)腮郊,運(yùn)行時(shí)默認(rèn)使用此類型。 每個(gè)范圍可以劃分為子范圍筹燕,從而實(shí)現(xiàn)負(fù)載平衡伴榔。 當(dāng)一個(gè)工作范圍完成時(shí),運(yùn)行時(shí)會(huì)將其他線程工作的子范圍重新分配給該線程庄萎。 如果您的工作負(fù)荷不在另外一個(gè)類別下或者您需要完全支持取消或協(xié)作阻塞,請使用該分區(qū)程序塘安。
concurrency::simple_partitioner
將工作劃分到范圍中糠涛,使每個(gè)范圍至少擁有給定區(qū)塊大小所指定的迭代的數(shù)目。 此分區(qū)程序類型加入了負(fù)載平衡兼犯;然而忍捡,運(yùn)行時(shí)未將范圍劃分為子范圍。 對于每個(gè)輔助切黔,運(yùn)行時(shí)將在 _Chunk_size
迭代完成后檢查取消情況并執(zhí)行負(fù)載平衡砸脊。
concurrency::static_partitioner
將工作劃分為一個(gè)固定數(shù)量的范圍(通常是可用于在循環(huán)中工作的輔助線程的數(shù)量)。 此分區(qū)程序類型不使用工作竊取纬霞,開銷較小凌埂,可以一定程度上提高性能。當(dāng)一個(gè)并行循環(huán)的每次迭代執(zhí)行固定和統(tǒng)一數(shù)量的工作而且您不需要支持取消或前向協(xié)作阻塞時(shí)诗芜,請使用此分區(qū)程序類型瞳抓。