### C++ 17 标准下的并行算法:std::execution::par 的使用与注意事项

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::parstd::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. 线程安全与副作用

并行算法要求 不产生副作用 或者 副作用是可组合且线程安全 的。常见错误包括:

  1. 写共享状态

    std::for_each(std::execution::par, vec.begin(), vec.end(), [](int x){ global_sum += x; });

    这里 global_sum 需要同步,否则结果不确定。

  2. 修改同一容器元素

    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::parstd::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_ifstd::reduce 并行算法,展示了 C++ 并行 STL 的强大与易用性。


7. 未来展望

  • C++20 引入了 std::execution::par_unseq(并行+向量化)与更丰富的执行策略。
  • 并行容器(如 concurrent_vector)的标准化仍在讨论。
  • OpenMPTBB 等第三方并行库的互操作性正在改善。

结语

std::execution::par 让 C++ 开发者在保持代码声明式与可维护性的同时,轻松利用多核 CPU 的计算能力。掌握其使用规则、线程安全原则与性能调优技巧,能够显著提升项目的运行效率。希望本文能帮助你在日常编码中正确、有效地使用并行算法。祝编码愉快!

发表评论