并行算法: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 之间的关系:

并行算法 执行策略 Lambda 表达式 数据安全 seq par par_unseq 捕获方式 线程安全 原子操作 互斥锁 无共享数据 核心:选对策略 + 写对 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++ 走向现代化的一个重要标志。用好它,你的程序就能在多核时代跑得更快。


公众号:蓝海资料掘金营,微信deep3321