C++17为标准库添加并行算法,都是对之前已存在的一些标准算法的重载,例如:std::find
,std::transform
和std::reduce
。并行版本的函数,在参数列表的中新添加了一个参数(指定要使用的执行策略)。
通过指定执行策略,算法的需求复杂性已经发生变化,并且要比串行版的算法要求要宽松。因为并行算法要利用系统的并行性,从而算法通常会做更多的工作。
1 执行策略
在<execution>
头文件中定义了三个相关的策略对象可以传递到算法中,分别对应三种策略(开发者不能自定义执行策略):
- std::execution::seq : std::execution::sequenced_policy
- std::execution::par : std::execution::parallel_policy
std::execution::par_unseq : std::execution::parallel_unsequenced_policy
1.1 策略对算法的影响
将执行策略传递给标准算法库中的算法,那么算法的行为就由执行策略控制。这会有几方面的影响:
算法复杂化:除了对并行的管理调度开销外,并行算法的核心操作将会被多次执行(交换,比较,以及提供的函数对象),目的是在总运行时间方面提供性能的整体改进。
- 抛出异常时的行为:如果有异常未被捕获,那么标准执行策略都会调用
std::terminate
。如果标准库无法提供给内部操作足够的资源,则在无执行策略算法执行时,会触发std::bad_alloc
异常。 算法执行的位置、方式和时间
需要在同一线程上执行所有操作,而且必须按照一定的顺序进行执行,这样步骤间就不会有交错
对算法使用的迭代器、相关值和可调用对象没什么要求:
对算法所使用的迭代器、相关值和可调用对象有了额外的要求:
使用并行不排序策略调用的算法,可以在任意线程上执行,这些线程彼此间没有顺序
- 算法使用的迭代器、相关值和可调用对象不能使用任何形式的同步,也不能调用任何需要同步的函数。
1.5 执行策略的选择
- std::execution::par是最常使用的策略,除非实现提供了更适合的非标准策略。如果代码适合并行化,那应该与std::execution::par一起工作。
某些情况下,可以使用std::execution::par_unseq进行代替。这可能根本没什么用(没有任何标准的执行策略可以保证能达到并行性的级别),但它可以给库额外的空间,通过重新排序和交错任务执行来提高代码的性能,以换取对代码更严格的要求(不能使用任何同步)。
2 标准库并行算法
标准库中大多数被重载的并行算法都在
<algorithm>
和<numeric>头文件
中:all_of,any_of,none_of,for_each,for_each_n,find,find_if,find_end,find_first_of,adjacent_find,count,count_if,mismatch,equal,search,search_n
- copy,copy_n,copy_if,move,swap_ranges,transform,replace,replace_if,replace_copy,replace_copy_if,fill,fill_n,generate,generate_n,remove
- remove_if,remove_copy,remove_copy_if,unique,unique_copy,reverse,reverse_copy,rotate,rotate_copy,is_partitioned,partition,stable_partition
- partition_copy,sort,stable_sort,partial_sort,partial_sort_copy,is_sorted,is_sorted_until,nth_element,merge,inplace_merge,includes,set_union
- set_intersection,set_difference,set_symmetric_difference,is_heap,is_heap_until,min_element,max_element,minmax_element,lexicographical_compare
- reduce,transform_reduce,exclusive_scan,inclusive_scan,transform_exclusive_scan,transform_inclusive_scan和adjacent_difference
对于列表中的每一个算法,每个”普通”算法的重载都有一个新的参数(第一个参数),这个参数将传入执行策略。
2.1 使用示例
最简单的例子就是并行循环:对容器的每个元素进行处理。这是令人尴尬的并行场景:每个项目都是独立的,所以有最大的并行性:
/*
使用标准库并行算法for_each的示例
*/
#include <algorithm>
#include <vector>
#include <iostream>
#include <execution>
void do_each(int &a)
{
a++;
std::cout << " after add, a = " << a << std::endl;
}
int main()
{
std::vector<int> v = {1, 2, 3, 4, 5};
// 使用并行算法,对vector中每个元素进行并行操作
std::for_each(std::execution::par, v.begin(), v.end(), do_each);
// 打印最后结果
for (int i = 0; i < v.size(); i++)
{
std::cout << v[i] << " " << std::endl;
}
return 0;
}
标准库将创建内部线程,并对数据进行划分,且对每个元素x调用do_stuff(x)。其中在线程间划分元素是一个实现细节。