C++17 引入了对标准算法的并行支持,为程序员提供了一种无缝地将算法并行化的机制。相比手写多线程代码,使用 std::execution::par 或 std::execution::par_unseq 可以让你在保持原有算法语义的前提下,利用多核 CPU 的计算能力。下面从概念、实现细节、使用案例以及注意事项四个方面,系统地讲解如何在 C++17 标准库中使用并行算法。
1. 并行算法的基本概念
标准算法本身是一系列基于迭代器的泛型算法,通常在单线程中顺序执行。C++17 在 `
` 中加入了 `std::execution` 命名空间,定义了三种执行策略: | 策略 | 说明 | |——|——| | `std::execution::seq` | 顺序执行,默认行为 | | `std::execution::par` | 并行执行,使用多线程,保持每个算法的原始迭代顺序(但不保证返回值的顺序) | | `std::execution::par_unseq` | 并行且向量化执行,允许在任意顺序执行并可能利用 SIMD 向量指令 | 在调用算法时,只需在前面加上执行策略即可,例如: “`cpp std::vector v = …; std::sort(std::execution::par, v.begin(), v.end()); “` 该调用将把排序过程拆分成多个子任务,分别在不同线程中并行完成,然后合并结果。 ## 2. 适用场景与限制 ### 适用场景 1. **大规模数据**:数组、向量、链表等容器中元素数目足够大,能够抵消线程创建与上下文切换的开销。 2. **CPU 密集型运算**:如排序、映射、归约等需要大量计算,而不是 I/O 或同步等待。 3. **无副作用**:并行执行要求算法内部不产生副作用(不写全局状态、无锁等),否则可能导致数据竞争。 ### 限制与陷阱 – **迭代器类型**:并行版本只支持随机访问迭代器(`RandomAccessIterator`)。链表、双向链表等迭代器无法使用。 – **算法本身的并行化**:标准库内部已经对常用算法进行了并行实现,但其实现细节不公开。若使用第三方算法,需要自行确认其并行性。 – **线程安全**:并行执行时,算法内部仍使用内部锁或原子操作保证正确性,性能受限于内部实现。 – **异常安全**:若算法抛出异常,所有子线程会被取消;因此使用并行算法时需确保异常处理与资源释放健壮。 ## 3. 示例:并行求和与归约 下面给出一个完整示例,演示如何使用 `std::reduce` 并行求和,并比较顺序与并行版本的性能差异。 “`cpp #include #include #include #include #include int main() { const std::size_t N = 100’000’000; std::vector data(N, 1); // 每个元素为 1 // 顺序求和 auto t0 = std::chrono::high_resolution_clock::now(); long long sum_seq = std::accumulate(data.begin(), data.end(), 0LL); auto t1 = std::chrono::high_resolution_clock::now(); std::cout << "Sequential sum = " << sum_seq << " time = " << std::chrono::duration_cast(t1 – t0).count() << " ms\n"; // 并行归约 auto t2 = std::chrono::high_resolution_clock::now(); long long sum_par = std::reduce(std::execution::par, data.begin(), data.end(), 0LL); auto t3 = std::chrono::high_resolution_clock::now(); std::cout << "Parallel sum = " << sum_par << " time = " << std::chrono::duration_cast(t3 – t2).count() << " ms\n"; return 0; } “` **运行结果(示例,实际取决于 CPU)**: “` Sequential sum = 100000000 time = 250 ms Parallel sum = 100000000 time = 60 ms “` 可以看到,使用并行算法大幅度缩短了执行时间。 ## 4. 并行排序与自定义比较 排序是最常见的需要并行化的算法之一。使用 `std::sort` 的并行版本时,标准库内部会将范围划分为若干子范围,每个子范围在独立线程中排序,最后再合并。以下示例演示自定义比较器: “`cpp struct Ascending { bool operator()(int a, int b) const { return a < b; } }; std::vector v = …; std::sort(std::execution::par, v.begin(), v.end(), Ascending()); “` 注意:自定义比较器必须是**无副作用**的函数对象,且在多线程环境下不能修改外部状态。 ## 5. 进阶技巧:自定义执行策略 如果你想更细粒度地控制并行行为(例如限制线程数、设置调度策略),可以自定义 `std::execution::sequenced_policy` 或 `std::execution::parallel_policy`。示例: “`cpp #include #include struct CustomParPolicy : std::execution::parallel_policy { CustomParPolicy(std::size_t threads) : threads_(threads) {} std::size_t threads() const noexcept { return threads_; } private: std::size_t threads_; }; CustomParPolicy par_policy(4); // 使用 4 线程 std::sort(par_policy, v.begin(), v.end()); “` 此方法允许你根据系统资源动态调整并行度。 ## 6. 性能调优建议 1. **避免过小的数据量**:并行化的开销可能大于收益,推荐数据量至少数十万以上。 2. **使用 `par_unseq`**:若算法本身是向量化友好的(无分支、循环不依赖前一次迭代),可尝试 `par_unseq`,进一步提升速度。 3. **预先分配内存**:如排序前使用 `reserve`,避免内部 realloc 造成额外开销。 4. **结合 `std::ranges`**:C++20 的 ranges 与 execution 策略结合,可写出更简洁的代码。 ## 7. 结语 C++17 并行算法提供了一种简洁且类型安全的方式,将常见算法并行化。你无需深入多线程细节,只需关注算法本身与数据结构,标准库会自动为你完成线程拆分、同步与合并。正确使用并行策略可以在不改变程序逻辑的前提下,显著提升性能。祝你在 C++ 并行编程的旅程中获得更多收获!