并行算法:std::execution::par 与 Lambda 在并行中的应用
多核处理器早就普及了,可我们写的 C++ 代码,很多时候还是单核在跑。说白了,就是没把 CPU 的潜力榨干。C++17 引入的并行算法,配合 Lambda 表达式,让我们能用很简洁的方式写出多线程代码。我个人觉得,这是现代 C++ 最实用的特性之一。
从串行到并行:一个简单的转变
先看个最基础的例子。假设我们要对一组数字做平方运算:
std::vector<int> data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 串行版本
std::for_each(data.begin(), data.end(), [](int& x) {
x = x * x;
});
// 并行版本
std::for_each(std::execution::par, data.begin(), data.end(), [](int& x) {
x = x * x;
});
看到了吗?就多了一个 std::execution::par 参数。但背后的机制完全不同。串行版本是一个接一个处理,并行版本会把数据分块,交给多个线程同时处理。
关键点:std::execution::par 告诉标准库:「这个算法可以并行执行」。但具体怎么分、分几块,由标准库的实现决定。你不需要手动管理线程。
三种执行策略
C++17 定义了三种执行策略,我整理成了表格:
| 策略 | 含义 | 适用场景 |
|---|---|---|
std::execution::seq |
顺序执行,单线程 | 调试、数据依赖强 |
std::execution::par |
并行执行,多线程 | 无数据竞争的计算密集型任务 |
std::execution::par_unseq |
并行 + 向量化 | 允许 CPU 自动向量化,性能极致 |
嗯,这里要注意:par_unseq 对 Lambda 的要求更严格。它要求 Lambda 内部不能有任何同步操作(比如加锁),因为向量化后的代码执行顺序是完全不可预测的。
Lambda 在并行中的角色
Lambda 在这里扮演的是「工作单元」的角色。每个线程拿到数据后,就执行这个 Lambda。所以 Lambda 必须是线程安全的。
我在项目中遇到过一个问题:有个同事在并行 Lambda 里修改了共享变量,结果数据全乱了。看这个反面教材:
int sum = 0;
std::vector<int> data(1000, 1);
// 错误!数据竞争
std::for_each(std::execution::par, data.begin(), data.end(), [&sum](int x) {
sum += x; // 多个线程同时写 sum,结果不确定
});
正确的做法是用 std::reduce 或者加原子操作:
std::atomic<int> sum{0};
std::for_each(std::execution::par, data.begin(), data.end(), [&sum](int x) {
sum.fetch_add(x, std::memory_order_relaxed);
});
我曾经踩过的坑:在并行 Lambda 里用了 std::vector::push_back。这玩意儿不是线程安全的,跑出来的结果每次都不一样。排查了一下午才发现。后来改用 std::vector::reserve 预分配空间,再用下标访问,问题就解决了。
并行算法的知识体系
我画了一张图,帮你理清并行算法、执行策略和 Lambda 之间的关系:
哪些算法支持并行?
不是所有算法都支持并行。C++17 标准库中,大约有 60 多个算法支持执行策略参数。我挑几个常用的说说:
- for_each:最常用,对每个元素执行操作
- sort:并行排序,大数据集效果明显
- reduce:并行归约,适合求和、求积
- transform:并行转换,一个输入变一个输出
- find:并行查找,找到就提前返回
小技巧:我习惯先用 std::execution::seq 调试,确保逻辑正确。然后改成 std::execution::par 做性能测试。如果数据量小于 1000,并行反而可能更慢——线程创建和上下文切换的开销比串行还大。
实战:并行排序 + Lambda
来看一个完整的例子。假设我们有一个学生成绩列表,要按总分排序:
struct Student {
std::string name;
int chinese, math, english;
};
std::vector<Student> students = {
{"Alice", 85, 92, 78},
{"Bob", 90, 88, 95},
{"Charlie", 76, 85, 90}
};
// 并行排序,按总分降序
std::sort(std::execution::par, students.begin(), students.end(),
[](const Student& a, const Student& b) {
int sumA = a.chinese + a.math + a.english;
int sumB = b.chinese + b.math + b.english;
return sumA > sumB; // 降序
});
这个 Lambda 只读取数据,不修改任何共享状态,所以是线程安全的。你想想看,如果数据量是 100 万条,串行排序可能要几秒,并行排序可能几百毫秒就搞定了。
避坑指南
我总结了几条实战经验,希望能帮你少走弯路:
- Lambda 里别用静态变量:多个线程共享一个静态变量,结果不可预测
- 小心迭代器失效:并行算法执行期间,不要修改容器大小
- 异常处理要谨慎:如果 Lambda 抛出异常,标准库会调用
std::terminate - 性能测试再决定:不是所有场景都适合并行,小数据集用串行更快
我曾经犯过的错:在并行 for_each 的 Lambda 里调用了 std::cout。结果输出全混在一起,根本看不清。后来改用 std::stringstream 先拼字符串,最后统一输出,才解决问题。
并行算法配合 Lambda,说白了就是让标准库帮你管理线程,你只管写业务逻辑。我个人觉得,这是 C++ 走向现代化的一个重要标志。用好它,你的程序就能在多核时代跑得更快。