C++ 17 引入了并行算法支持,允许程序员在标准算法中显式指定执行策略,从而在多核 CPU 上自动并行化常见的 STL 容器操作。最常用的执行策略是 std::execution::par,它会让算法在内部使用多线程执行。本文将详细介绍如何在实际项目中使用 std::execution::par,以及需要注意的坑与最佳实践。
1. 基础使用
#include <algorithm>
#include <execution>
#include <vector>
#include <iostream>
int main() {
std::vector <int> data(1'000'000);
std::iota(data.begin(), data.end(), 1); // 生成 1..1e6
// 并行求和
long long sum = std::reduce(std::execution::par, data.begin(), data.end(), 0LL);
std::cout << "sum = " << sum << '\n';
}
std::execution::par让std::reduce在内部分割任务,利用多线程并行执行。- 如果不指定执行策略,算法默认使用顺序执行
std::execution::seq。
2. 适用场景
| 算法 | 是否支持并行 | 适用场景 | 说明 |
|---|---|---|---|
std::sort |
✅ | 大规模数据排序 | 内部使用 introsort,支持并行 quicksort |
std::for_each |
✅ | 并行遍历 | 每个元素可并行执行无副作用 |
std::reduce |
✅ | 并行聚合 | 需要满足无顺序依赖的可组合运算 |
std::transform |
✅ | 并行映射 | 输出元素独立于输入 |
std::count_if |
✅ | 并行计数 | 判断条件不产生副作用 |
std::find_if |
❌ | 单次查找 | 需要顺序保证,使用 seq |
注意:并非所有 STL 算法都支持并行执行。使用不支持的策略会导致编译错误。
3. 线程安全与副作用
并行算法要求 不产生副作用 或者 副作用是可组合且线程安全 的。常见错误包括:
-
写共享状态
std::for_each(std::execution::par, vec.begin(), vec.end(), [](int x){ global_sum += x; });这里
global_sum需要同步,否则结果不确定。 -
修改同一容器元素
std::transform(std::execution::par, vec.begin(), vec.end(), vec.begin(), [](int x){ return x * 2; });上面代码是安全的,因为每个元素只写一次。
但如果你使用std::for_each对同一元素多次写入,可能导致 race 条件。
解决方案:
- 使用局部变量累加后再一次写入共享变量。
- 使用
std::atomic。- 或者使用
std::execution::par_unseq并确保元素操作是无副作用的。
4. 性能调优
| 调优点 | 做法 | 备注 |
|---|---|---|
| 线程数 | std::execution::par 采用 std::thread::hardware_concurrency() 默认值。可通过 std::execution::par 与 std::execution::par_unseq 结合自定义 std::launch::async? |
目前 C++ 17 标准没有直接设置线程数的机制,需要自己实现并行包装器或使用第三方库(如 TBB, OpenMP)。 |
| 数据分割 | 默认分块策略适合大数据;若数据量小,可使用 std::execution::seq |
过小块导致线程创建开销大于并行收益。 |
| 内存访问 | 避免共享缓存冲突,尽量使每个线程访问独立内存 | 能够显著提升 cache 命中率 |
| 任务粒度 | 大任务并行,小任务顺序执行 | 例如对 vector 1M 元素进行排序,使用 par;对 10 条数据使用 seq。 |
5. 常见坑与调试技巧
| 坑 | 解决办法 |
|---|---|
| 算法崩溃或结果错误 | 检查是否存在共享状态写冲突。使用 std::atomic 或局部变量累加再写入。 |
| 性能不提升 | 1) 数据量太小。2) CPU 线程数受限。3) 内存带宽瓶颈。4) 线程创建销毁开销。 |
| 多线程异常 | C++ 并行算法会捕获异常并在主线程抛出 std::execution::parallel_algorithm_exception。确保异常可恢复。 |
| 编译错误 | 检查是否包含 ` |
并使用-std=c++17` 或更高。 |
6. 示例:并行筛选 + 求和
#include <execution>
#include <algorithm>
#include <numeric>
#include <vector>
#include <iostream>
int main() {
std::vector <int> data(10'000'000);
std::iota(data.begin(), data.end(), 1);
// 并行筛选偶数
std::vector <int> evens;
evens.reserve(data.size() / 2); // 预估大小
std::copy_if(std::execution::par, data.begin(), data.end(),
std::back_inserter(evens), [](int x){ return x % 2 == 0; });
// 并行求和
long long sum = std::reduce(std::execution::par, evens.begin(), evens.end(), 0LL);
std::cout << "偶数和: " << sum << '\n';
}
该示例演示了组合使用
std::copy_if与std::reduce并行算法,展示了 C++ 并行 STL 的强大与易用性。
7. 未来展望
- C++20 引入了
std::execution::par_unseq(并行+向量化)与更丰富的执行策略。 - 并行容器(如
concurrent_vector)的标准化仍在讨论。 - 与 OpenMP、TBB 等第三方并行库的互操作性正在改善。
结语
std::execution::par 让 C++ 开发者在保持代码声明式与可维护性的同时,轻松利用多核 CPU 的计算能力。掌握其使用规则、线程安全原则与性能调优技巧,能够显著提升项目的运行效率。希望本文能帮助你在日常编码中正确、有效地使用并行算法。祝编码愉快!