c++线程之标准库的并行算法研究

C++ 标准库并行算法

C++17正式引入了标准库并行算法(后续C++20/C++23有小幅扩展),这些算法基于** 执行策略(Execution Policy) **实现并行控制,无需用户手动管理线程,大幅简化了高性能并行编程的复杂度。并行算法主要分布在<algorithm>(序列算法)和<numeric>(数值算法)头文件中,且必须依赖<execution>头文件提供执行策略支持。

1、核心:执行策略(并行行为的驱动核心)

执行策略是并行算法的第一个参数,用于指定算法的执行模式,直接决定了算法的并行行为和性能表现。C++标准在std::execution命名空间下提供了三种核心执行策略:

1. 顺序执行策略:std::execution::seq

  • 特性:本质是串行执行,无任何并行优化,仅作为并行算法的基准(方便串行/并行模式切换测试)。
  • 执行方式:单线程按原有串行算法的元素处理顺序执行,无任务拆分、线程调度开销。
  • 适用场景:小数据量、调试阶段、存在依赖关系的非并行任务。

2. 并行执行策略:std::execution::par

  • 特性:多线程并行执行,标准库管理内置线程池,任务拆分后同步执行。
  • 执行方式
    1. 库自动将整体任务拆分为独立子任务(基于数据区间分块);
    2. 从内置线程池分配线程执行子任务(避免线程频繁创建/销毁的开销);
    3. 多线程同时执行子任务,算法整体阻塞(直到所有子任务完成才返回);
    4. 允许子任务按任意顺序执行,但要求无数据竞争(用户保证)。
  • 适用场景:大部分并行场景(排序、批量遍历、无依赖转换),是最常用的并行策略。

3. 并行+向量执行策略:std::execution::par_unseq

  • 特性 :更激进的并行优化,结合了多线程并行单线程SIMD向量执行(如CPU的SSE/AVX指令集,单指令处理多个数据)。
  • 执行方式
    1. 同时支持跨线程并行(同par策略)和线程内SIMD批量执行;
    2. 子任务可能被库重新排序、迁移(跨线程),约束更强;
    3. 要求子任务完全独立(无任何依赖关系),禁止阻塞操作(如std::mutex锁定)。
  • 适用场景:无依赖、纯计算、大数据量的密集型任务(如大规模数值计算),可获得极致性能。

2、常用并行算法及可运行示例

类别1:非修改式序列算法(std::for_each

std::for_each是基础并行算法,用于批量遍历并处理元素(只读或线程安全写入),并行版本可大幅提升遍历效率。

用途

批量处理容器元素(打印元素、统计特征、只读转换等)。

完整代码
cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
#include <execution>
#include <thread>  // 获取当前线程ID,验证并行性

// 元素处理函数:打印元素值和所属线程ID
void processElement(int val) {
    std::cout << "线程ID: " << std::this_thread::get_id()
              << ", 元素值: " << val << std::endl;
}

int main() {
    // 初始化测试容器
    std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    // 并行执行for_each,使用par策略(第一个参数为执行策略)
    std::for_each(std::execution::par, vec.begin(), vec.end(), processElement);

    return 0;
}
执行方式解析
  1. 任务拆分 :标准库将vec的迭代器区间[begin(), end())拆分为若干子区间(如[1,2,3][4,5,6][7,8,9,10],粒度由库自动优化);
  2. 线程调度:从内置线程池分配多个线程,每个线程负责一个子区间;
  3. 并行执行 :多个线程同时调用processElement处理各自子区间的元素,输出会显示多个不同的线程ID(体现并行性);
  4. 同步返回 :所有元素处理完成后,std::for_each才会返回,main函数继续执行。

类别2:修改式序列算法

1. std::sort(并行排序)
用途

对容器元素进行排序,大数据量下并行版本性能远超串行std::sort

完整代码
cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
#include <execution>
#include <random>  // 生成随机测试数据

int main() {
    // 1. 生成10万个随机整数(大数据量体现并行优势)
    std::vector<int> vec(100000);
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<> dis(1, 10000);
    for (int i = 0; i < vec.size(); ++i) {
        vec[i] = dis(gen);
    }

    // 2. 并行排序:使用par策略
    std::sort(std::execution::par, vec.begin(), vec.end());

    // 3. 验证排序结果(打印前10个元素)
    std::cout << "并行排序后前10个元素:";
    for (int i = 0; i < 10; ++i) {
        std::cout << vec[i] << " ";
    }
    std::cout << std::endl;

    return 0;
}
执行方式解析
  1. 任务拆分:采用并行分治策略(如并行快速排序/归并排序),将数组拆分为多个子数组;
  2. 并行排序:多个线程同时排序各自的子数组;
  3. 结果合并:线程协作合并有序子数组,形成全局有序数组;
  4. 同步返回 :合并完成后,std::sort返回,此时vec已完全有序。
2. std::transform(并行数据转换)
用途

批量将输入容器的元素转换后存入输出容器,支持无依赖并行执行。

完整代码
cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
#include <execution>

// 转换函数:将元素翻倍
int doubleElement(int val) {
    return val * 2;
}

int main() {
    // 输入容器
    std::vector<int> input = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    // 输出容器:提前分配空间,避免并行写入时的内存竞争
    std::vector<int> output(input.size());

    // 并行转换:input元素翻倍后存入output
    std::transform(std::execution::par,
                   input.begin(), input.end(),
                   output.begin(),
                   doubleElement);

    // 打印转换结果
    std::cout << "并行转换后结果:";
    for (int val : output) {
        std::cout << val << " ";
    }
    std::cout << std::endl;

    return 0;
}
执行方式解析
  1. 任务拆分 :将inputoutput按相同粒度拆分为子区间;
  2. 并行转换 :多个线程同时处理各自子区间,调用doubleElement转换元素并写入output对应位置(每个位置仅被一个线程写入,无数据竞争);
  3. 同步返回 :所有元素转换完成后,std::transform返回。

类别3:数值算法(std::reduce,并行聚合)

注意:std::accumulate是串行聚合算法,不支持执行策略;std::reduce是其并行版本,要求聚合操作满足结合律

用途

对容器元素进行聚合计算(累加、乘积等),大数据量下快速聚合。

完整代码
cpp 复制代码
#include <iostream>
#include <vector>
#include <numeric>  // reduce/accumulate头文件
#include <execution>

int main() {
    // 1. 生成100万个1的容器(大数据量测试)
    std::vector<int> vec(1000000, 1);

    // 2. 并行累加:par策略,初始值0,默认加法操作
    int parallel_sum = std::reduce(std::execution::par, vec.begin(), vec.end(), 0);

    // 3. 串行累加(作为对比)
    int serial_sum = std::accumulate(vec.begin(), vec.end(), 0);

    // 4. 打印结果
    std::cout << "并行累加结果:" << parallel_sum << std::endl;
    std::cout << "串行累加结果:" << serial_sum << std::endl;

    return 0;
}
执行方式解析
  1. 任务拆分 :将vec拆分为若干子区间,每个子区间对应一个局部累加任务;
  2. 并行局部累加:多个线程同时计算各自子区间的局部和(如4个子区间的局部和各为250000);
  3. 全局汇总:所有局部和计算完成后,库将局部和再次累加(单线程/多线程),得到最终全局和;
  4. 同步返回 :汇总完成后,std::reduce返回结果。
  • 关键特性:加法满足结合律,因此拆分/汇总顺序不影响结果;若使用减法(不满足结合律),并行结果会与串行结果不一致。

3、并行算法的通用执行流程

无论哪种并行算法,其底层执行都遵循以下5个核心步骤,这是理解并行算法工作机制的关键:

  1. 任务拆分
    标准库根据输入数据规模、容器迭代器类型(随机访问迭代器最优,如vector/array)和执行策略,自动将整体任务拆分为独立无依赖的子任务,拆分方式以「数据区间分块」为主,粒度由库内部优化(无需用户干预)。
  2. 线程调度
    对于par/par_unseq策略,标准库使用内置线程池分配线程(避免线程频繁创建/销毁的开销)。若子任务数量多于线程池线程数,库会进行任务调度(如抢占式调度),让线程完成一个子任务后继续执行下一个。
  3. 并行执行
    • par策略:子任务在多个线程上并发执行(真正多线程并行),无固定执行顺序;
    • par_unseq策略:子任务既跨线程并行,又在单线程内通过SIMD向量指令 批量执行(如一次处理8/16个数据),实现双重优化。
      此阶段要求用户保证子任务无数据竞争(只读数据安全,独立写入安全,共享写入需同步)。
  4. 结果汇总(仅聚合类算法)
    对于std::reduce等聚合类算法,先计算各子任务的局部结果,再通过聚合操作合并为全局结果,汇总过程可再次并行化,核心依赖操作的结合律。
  5. 同步返回
    所有并行算法均为阻塞式执行 :调用线程会暂停,直到所有子任务执行完成、结果汇总完毕后,才会继续执行。若需要异步并行,需用户结合std::async手动封装。

4、关键注意事项

  1. 头文件完整性 :必须包含<execution>(执行策略)+ 对应算法头文件(<algorithm>/<numeric>),否则编译器报错。
  2. 避免数据竞争 :共享数据写入时,需使用std::atomic(原子类型)或std::mutex(互斥锁)保证同步;优先使用独立内存写入(如transform的输出容器)。
  3. 策略选择原则
    • 优先用par:兼容性好、约束少,满足大部分场景;
    • 仅大数据量无依赖场景用par_unseq:避免约束违规导致未定义行为;
    • 小数据量用seq:并行开销(拆分/调度)可能超过收益,串行更快。
  4. 迭代器影响性能 :随机访问迭代器(vector/array)并行效率最高;双向迭代器(list/set)并行效率低;输入迭代器(istream_iterator)不支持并行。
  5. 结合律要求std::reduce等聚合算法要求操作满足结合律,否则结果可能不一致。

总结

  1. C++17并行算法基于执行策略seq/par/par_unseq)驱动,无需手动管理线程,核心头文件为<execution>+<algorithm>/<numeric>
  2. 常用并行算法包括std::for_each(遍历)、std::sort(排序)、std::transform(转换)、std::reduce(聚合),各有明确适用场景;
  3. 并行算法底层执行流程为「任务拆分→线程调度→并行执行→结果汇总(聚合类)→同步返回」;
  4. 关键优化点:优先使用par策略、选择随机访问迭代器容器、保证无数据竞争、仅大数据量场景启用并行。

C++17 的三种执行策略研究

已在上个章节的《核心:执行策略(并行行为的驱动核心)》进行了简单阐述,下面详细举例说明

1、std::execution::sequenced_policy(顺序执行策略)

1. 核心特性

  • 官方别名std::execution::seq(实际编码中更常用该别名,与完整名称等价)
  • 执行模式:纯串行执行,无任何并行优化,不创建额外线程
  • 核心约束:严格按照容器元素的迭代器顺序处理数据,无任务拆分、无线程调度开销
  • 适用场景:小数据量处理、调试阶段(方便跟踪执行流程)、元素处理存在依赖关系(无法并行)、并行开销大于收益的场景

2. 完整示例

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
#include <execution>
#include <thread>

// 元素处理函数:打印元素值和所属线程ID
void processSeqElement(int val) {
    // 打印线程ID,验证是否为单线程执行
    std::cout << "线程ID: " << std::this_thread::get_id()
              << " | 处理元素: " << val 
              << "(顺序执行,按元素索引递增处理)" << std::endl;
}

int main() {
    // 初始化测试容器
    std::vector<int> vec = {1, 2, 3, 4, 5};

    // 使用顺序执行策略(完整名称/std::execution::seq别名均可)
    std::for_each(std::execution::sequenced_policy(),  // 完整策略名称
                  vec.begin(), vec.end(),
                  processSeqElement);

    // 等价写法(更简洁,实际开发优先使用)
    // std::for_each(std::execution::seq, vec.begin(), vec.end(), processSeqElement);

    return 0;
}

3. 代码与执行机制解析

  • 代码说明
    1. 采用std::for_each算法,第一个参数传入std::execution::sequenced_policy()(完整名称)或其别名std::execution::seq
    2. 处理函数processSeqElement打印当前线程ID和元素值,用于验证串行特性;
    3. 测试容器vec包含5个有序元素,便于观察执行顺序。
  • 执行机制
    1. 无任务拆分:标准库不会对vec的迭代器区间进行拆分,整体作为一个单任务处理;
    2. 单线程执行:仅使用调用线程(main线程)执行所有元素的处理逻辑,输出中所有元素对应的线程ID完全一致;
    3. 严格按序执行:元素会按照1→2→3→4→5的顺序依次处理,不会出现乱序输出,完全遵循迭代器的遍历顺序;
    4. 无额外开销:无需线程创建、调度、同步等开销,执行流程与传统串行算法完全一致。

2、std::execution::parallel_policy(并行执行策略)

1. 核心特性

  • 官方别名std::execution::par(实际编码中更常用该别名)
  • 执行模式:多线程并行执行,由标准库管理内置线程池(避免线程频繁创建/销毁的开销)
  • 核心约束
    1. 子任务可按任意顺序执行(元素处理顺序不保证);
    2. 用户需保证元素处理无数据竞争(如共享数据写入需同步);
    3. 算法为阻塞式执行(调用线程等待所有子任务完成后返回)。
  • 适用场景:大部分通用并行场景(如大数据量排序、批量遍历、无依赖数据转换),是日常并行编程的首选策略。

2. 完整示例

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
#include <execution>
#include <thread>
#include <random>

// 元素处理函数:打印元素值和所属线程ID
void processParElement(int val) {
    std::cout << "线程ID: " << std::this_thread::get_id()
              << " | 处理元素: " << val << std::endl;
}

int main() {
    // 初始化10个元素的测试容器(数据量适中,便于观察并行效果)
    std::vector<int> vec(10);
    // 生成随机数填充容器(打乱初始顺序,更易观察并行乱序执行)
    std::random_device rd;
    std::mt19937 gen(rd());
    std::iota(vec.begin(), vec.end(), 1); // 先填充1-10
    std::shuffle(vec.begin(), vec.end(), gen); // 打乱顺序

    std::cout << "=== 并行执行策略(par)开始处理 ===" << std::endl;
    // 使用并行执行策略(别名std::execution::par,更简洁)
    std::for_each(std::execution::par,
                  vec.begin(), vec.end(),
                  processParElement);
    std::cout << "=== 并行执行策略(par)处理完成 ===" << std::endl;

    return 0;
}

3. 代码与执行机制解析

  • 代码说明
    1. 采用std::execution::par别名(实际开发首选,代码更简洁);
    2. 通过std::shuffle打乱容器元素顺序,便于观察并行执行的乱序特性;
    3. 处理函数打印线程ID,直观验证多线程并行效果。
  • 执行机制
    1. 任务拆分 :标准库自动将vec的迭代器区间[begin(), end())拆分为若干独立子区间(如[3,7,1][10,2,5][4,6,8,9],拆分粒度由库自动优化);
    2. 线程调度:从内置线程池分配多个线程(数量通常与CPU核心数匹配),每个线程负责一个子区间的处理任务;
    3. 并行执行 :多个线程同时调用processParElement处理各自子区间的元素,输出中会出现多个不同的线程ID,且元素处理顺序混乱(无固定顺序);
    4. 同步返回 :所有子区间的元素处理完成后,std::for_each才会返回,main函数继续执行后续逻辑(阻塞式执行);
    5. 无额外线程开销:依赖内置线程池复用线程,避免了频繁创建/销毁线程的性能损耗。

3、std::execution::parallel_unsequenced_policy(并行+向量执行策略)

1. 核心特性

  • 官方别名std::execution::par_unseq(实际编码中更常用该别名)
  • 执行模式:最激进的性能优化,结合了「多线程并行」和「单线程SIMD向量执行」(如CPU的SSE/AVX指令集,单指令同时处理多个数据)
  • 核心约束 (比parallel_policy更严格):
    1. 子任务完全独立(无任何数据依赖、无跨任务同步);
    2. 禁止阻塞操作(如std::mutex锁定、线程休眠等);
    3. 子任务可能被标准库重新排序、跨线程迁移;
    4. 仅支持随机访问迭代器(如vector/array,不支持list/set)。
  • 适用场景:大数据量、纯计算、无依赖的密集型任务(如大规模数值转换、批量数学计算),追求极致执行性能。

2. 完整示例

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
#include <execution>
#include <numeric>

// 纯计算转换函数:元素平方(无依赖、无阻塞、纯CPU计算,适配par_unseq)
int squareElement(int val) {
    return val * val;
}

int main() {
    // 初始化100万个元素的大数据量容器(体现par_unseq的性能优势)
    std::vector<int> input(1000000);
    std::iota(input.begin(), input.end(), 1); // 填充1-1000000
    std::vector<int> output(input.size());    // 提前分配输出空间,避免内存竞争

    std::cout << "=== 并行+向量执行策略(par_unseq)开始转换 ===" << std::endl;
    // 使用并行+向量执行策略(别名std::execution::par_unseq)
    std::transform(std::execution::par_unseq,
                   input.begin(), input.end(),
                   output.begin(),
                   squareElement);
    std::cout << "=== 并行+向量执行策略(par_unseq)转换完成 ===" << std::endl;

    // 验证前10个和后10个元素的转换结果
    std::cout << "前10个元素转换结果:" << std::endl;
    for (int i = 0; i < 10; ++i) {
        std::cout << input[i] << " → " << output[i] << std::endl;
    }

    std::cout << "后10个元素转换结果:" << std::endl;
    for (int i = input.size() - 10; i < input.size(); ++i) {
        std::cout << input[i] << " → " << output[i] << std::endl;
    }

    return 0;
}

3. 代码与执行机制解析

  • 代码说明
    1. 采用std::execution::par_unseq别名,选择std::transform算法(纯数据转换,无依赖);
    2. 测试容器大小为100万(大数据量才能体现SIMD向量执行的优势);
    3. 转换函数squareElement为纯计算逻辑(无阻塞、无数据依赖),完全满足par_unseq的严格约束;
    4. 提前分配输出容器output的空间,每个元素仅被一个线程写入,避免数据竞争。
  • 执行机制
    1. 双层并行优化
      • 跨线程并行:同par策略,将输入区间拆分为子区间,由多个线程并行处理;
      • 线程内SIMD向量执行:单个线程通过CPU向量指令(如AVX-512),一次处理8/16个元素(而非逐个处理),大幅提升单线程处理效率;
    2. 灵活任务调度:标准库可根据系统负载,动态重新排序子任务、将子任务从一个线程迁移到另一个线程,最大化CPU利用率;
    3. 无阻塞约束:由于禁止阻塞操作,线程不会陷入等待,CPU核心始终处于高负载状态,保证极致性能;
    4. 同步返回 :与par策略一致,所有元素转换完成后,算法阻塞返回,输出容器output完全有效。

4、三种执行策略核心对比总结

执行策略完整名称 常用别名 执行模式 线程特性 核心约束 适用场景
sequenced_policy std::execution::seq 纯串行执行 单线程(调用线程) 严格按迭代器顺序执行 小数据量、调试、有依赖的任务
parallel_policy std::execution::par 多线程并行执行 多线程(内置线程池) 无数据竞争、允许乱序执行 大部分通用并行场景(排序、遍历等)
parallel_unsequenced_policy std::execution::par_unseq 多线程并行+SIMD向量执行 多线程+SIMD优化 无依赖、无阻塞、随机访问迭代器 大数据量、纯计算、极致性能需求

关键补充

  1. 三种策略均定义在<execution>头文件中,使用时必须包含该头文件;
  2. 实际编码中,优先使用别名(seq/par/par_unseq),代码更简洁易读;
  3. 策略选择优先级:par(通用场景)> seq(调试/小数据)> par_unseq(极致性能/无依赖)。

5、 20 std::execution::unsequenced_policy 策略

你需要了解C++20新增的std::execution::unsequenced_policy执行策略,该策略是对C++17并行执行策略的补充,主打「单线程+SIMD向量优化」,在特定场景下能以极低开销实现性能提升,下面将从核心特性、完整示例、执行机制及与其他策略的对比展开详细说明。

1、std::execution::unsequenced_policy 核心特性

1. 基础信息
  • 完整名称std::execution::unsequenced_policy
  • 常用别名std::execution::unseq(实际编码中优先使用该别名,更简洁易读)
  • 所属标准:C++20(需编译器支持C++20及以上标准,如GCC 10+、Clang 11+、MSVC 19.20+)
  • 核心头文件<execution>(使用时必须包含该头文件,同时搭配算法头文件<algorithm>/<numeric>
2. 核心执行模式

区别于C++17的三种策略,unsequenced_policy的核心是「单线程执行 + SIMD向量优化」,具体特性如下:

  1. 单线程特性 :不创建任何额外线程,仅使用调用线程(如main线程)执行任务,无线程创建、调度、同步的开销;
  2. SIMD向量优化:利用CPU的SIMD指令集(如SSE、AVX、AVX-512),实现「单指令多数据」批量处理,单个CPU指令可同时处理8/16个同类型元素(如一次计算8个整数的平方),大幅提升单线程处理效率;
  3. 执行顺序不保证 :虽然是单线程执行,但由于SIMD批量处理和库内部优化,元素的处理顺序不严格遵循迭代器顺序 (与seq的严格按序执行形成鲜明对比);
  4. 严格约束条件 (比seq严格,略低于par_unseq):
    • 元素处理必须完全独立(无数据依赖,如不能根据前一个元素的值修改当前元素);
    • 禁止阻塞操作(如std::mutex锁定、std::this_thread::sleep_for等);
    • 仅支持随机访问迭代器 (如std::vectorstd::array、原生数组,不支持std::liststd::set等双向/输入迭代器容器);
    • 无数据竞争(由于单线程执行,只读数据天然安全;写入数据时,每个内存位置仅被单次写入,无需额外同步)。
3. 适用场景
  • 小到中等数据量的纯计算任务(数据量过小时,SIMD优化的收益大于开销;数据量过大时,建议使用par_unseq);
  • 对线程开销敏感的场景(如嵌入式系统、低功耗设备,线程创建/调度的开销可能抵消并行收益);
  • 无法使用多线程的场景(如任务存在轻微线程安全风险,或系统线程资源紧张);
  • 纯数据转换/计算场景(如元素平方、数值累加、数据格式化等无依赖逻辑)。

2、完整可运行示例

下面以std::transform算法为例(纯数据转换,适配unseq策略的约束),展示unsequenced_policy的使用方式,代码可直接编译运行。

示例代码
cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
#include <execution>
#include <numeric>
#include <thread>

// 纯计算转换函数:计算元素的平方(无数据依赖、无阻塞、纯CPU操作,适配unseq策略)
int squareElement(int val) {
    return val * val;
}

int main() {
    // 1. 初始化测试容器:10万个整数(小到中等数据量,体现unseq的优势)
    std::vector<int> input(100000);
    std::iota(input.begin(), input.end(), 1); // 填充1~100000
    std::vector<int> output(input.size());    // 提前分配输出空间,避免内存竞争

    // 2. 打印调用线程ID(验证unseq的单线程特性)
    std::cout << "调用线程ID:" << std::this_thread::get_id() << std::endl;
    std::cout << "=== 开始使用unseq策略执行并行转换 ===" << std::endl;

    // 3. 使用unsequenced_policy(别名std::execution::unseq)执行transform
    std::transform(std::execution::unseq,
                   input.begin(), input.end(),
                   output.begin(),
                   squareElement);

    std::cout << "=== unseq策略转换完成 ===" << std::endl;

    // 4. 验证转换结果(打印前10个和后10个元素,确认正确性)
    std::cout << "前10个元素转换结果:" << std::endl;
    for (int i = 0; i < 10; ++i) {
        std::cout << input[i] << " → " << output[i] << "(预期值:" << input[i] * input[i] << ")" << std::endl;
    }

    std::cout << "后10个元素转换结果:" << std::endl;
    for (int i = input.size() - 10; i < input.size(); ++i) {
        std::cout << input[i] << " → " << output[i] << "(预期值:" << input[i] * input[i] << ")" << std::endl;
    }

    return 0;
}
编译与运行说明
  • 编译命令(GCC):g++ -std=c++20 main.cpp -o unseq_demo(需指定C++20标准)
  • 运行结果:所有转换结果与预期值一致,且仅输出一个线程ID(验证单线程特性),转换效率高于串行seq策略。

3、执行机制解析

std::execution::unseq的执行流程简单清晰,无跨线程调度逻辑,核心分为3步:

1. 任务批量划分(单线程内)

标准库根据CPU的SIMD指令集宽度(如AVX-512对应8个int类型元素),将输入迭代器区间[begin(), end())划分为若干批量数据块(而非跨线程子区间)。例如,对于10万个int元素,会划分为12500个批量块(每个块8个元素),批量大小由库自动适配CPU特性,无需用户干预。

2. SIMD向量并行执行(单线程内)

调用线程依次处理每个批量数据块,通过CPU的SIMD向量指令实现「批量计算」:

  • 传统串行(seq策略):逐个处理元素,每个指令仅处理1个int的平方计算;
  • unseq策略:批量处理元素,单个SIMD指令同时处理8个int的平方计算,相当于单线程内的「数据级并行」,大幅提升处理速度。
  • 注意:批量块的处理顺序可能与迭代器顺序一致,但块内元素的处理是并行的,且整体元素的处理顺序不保证严格按迭代器索引递增(库可能调整批量块顺序以优化缓存命中率)。
3. 同步返回(单线程阻塞)

与其他执行策略一致,std::transform为阻塞式执行:调用线程会依次处理所有批量数据块,直到所有元素转换完成后,算法才会返回,后续代码才能继续执行。由于无跨线程同步,其阻塞开销远低于parpar_unseq策略。

4、unseq与其他执行策略的核心对比

为了更清晰地理解unseq的定位,下面将其与C++17的三种策略进行核心对比:

执行策略(C++版本) 执行模式 线程数量 执行顺序 核心优势 适用场景
seq(C++17) 纯串行执行(无SIMD) 1个 严格按迭代器顺序 无开销、可调试、支持依赖 小数据量、有依赖、调试阶段
unseq(C++20) 单线程+SIMD向量执行 1个 不保证严格顺序 低开销、单线程安全、速度快 小中等数据量、纯计算、线程敏感场景
par(C++17) 多线程并行(无SIMD) 多个 不保证顺序 通用并行、兼容性好 大部分大数据量并行场景
par_unseq(C++17) 多线程+SIMD向量执行 多个 不保证顺序 极致性能、双重并行优化 超大数据量、纯计算、无依赖场景
关键差异总结
  1. unseq vs seq:两者均为单线程,但unseq有SIMD向量优化,速度更快;seq严格按序执行,支持元素依赖;
  2. unseq vs par_unseq:两者均有SIMD优化,但unseq是单线程(无线程开销),par_unseq是多线程(有线程调度开销),小数据量下unseq更优,大数据量下par_unseq更优;
  3. unseq vs parunseq单线程+SIMD,par多线程无SIMD,纯计算场景下unseq的单线程SIMD可能媲美甚至超过par的多线程(无线程开销)。

5、使用注意事项

  1. 编译器支持 :必须使用支持C++20的编译器,且部分编译器可能需要开启SIMD优化开关(如GCC的-mavx2-mavx512f);
  2. 迭代器限制 :仅支持随机访问迭代器,使用listmap等容器会编译报错;
  3. 约束遵守:必须保证元素处理无数据依赖、无阻塞操作,否则会导致未定义行为;
  4. 数据量选择 :小到中等数据量(如1万~100万元素)优先使用unseq,超大数据量(如1000万+元素)优先使用par_unseq
  5. 性能测试 :不同CPU的SIMD支持程度不同,实际使用时建议通过性能测试对比unseqseqpar的效果。

总结

  1. std::execution::unsequenced_policy(别名unseq)是C++20新增单线程执行策略,核心为「单线程+SIMD向量优化」;
  2. 其无线程开销、单线程安全,适合小中等数据量、纯计算、线程敏感的场景,执行顺序不保证,仅支持随机访问迭代器;
  3. seq相比有SIMD性能优势,与par_unseq相比有低开销优势,是C++执行策略中的「轻量级性能优化选项」;
  4. 实际编码中优先使用别名std::execution::unseq,搭配std::transformstd::reduce等算法实现高效单线程批量计算。

算法穷举

1、 列举一下

1. 排序算法

cpp 复制代码
std::sort(std::execution::par, begin, end);
std::stable_sort(std::execution::par, begin, end);
std::partial_sort(std::execution::par, begin, middle, end);
std::nth_element(std::execution::par, begin, nth, end);

含义:对范围进行排序,并行版本可加速大规模数据排序。

注意事项

  • 比较函数必须线程安全
  • 不稳定排序的相对顺序可能因并行化而改变
  • 数据竞争需避免

2. 数值算法

cpp 复制代码
std::reduce(std::execution::par, begin, end, init);
std::transform_reduce(std::execution::par, begin1, end1, begin2, init);
std::inclusive_scan(std::execution::par, begin, end, result);
std::exclusive_scan(std::execution::par, begin, end, result, init);
std::transform_inclusive_scan(std::execution::par, ...);
std::transform_exclusive_scan(std::execution::par, ...);

含义

  • reduce:并行版本的累加,要求操作满足结合律
  • transform_reduce:先变换再累加
  • scan:前缀和计算

注意事项

  • 操作必须满足结合律
  • 浮点运算顺序可能改变,影响精度
  • 避免副作用

3. 查找算法

cpp 复制代码
std::find(std::execution::par, begin, end, value);
std::find_if(std::execution::par, begin, end, predicate);
std::find_if_not(std::execution::par, begin, end, predicate);
std::find_end(std::execution::par, begin1, end1, begin2, end2);
std::find_first_of(std::execution::par, begin1, end1, begin2, end2);
std::search(std::execution::par, begin1, end1, begin2, end2);
std::search_n(std::execution::par, begin, end, count, value);
std::count(std::execution::par, begin, end, value);
std::count_if(std::execution::par, begin, end, predicate);
std::all_of(std::execution::par, begin, end, predicate);
std::any_of(std::execution::par, begin, end, predicate);
std::none_of(std::execution::par, begin, end, predicate);
std::adjacent_find(std::execution::par, begin, end);
std::mismatch(std::execution::par, begin1, end1, begin2);
std::equal(std::execution::par, begin1, end1, begin2);

注意事项

  • 谓词必须是纯函数(无副作用)
  • 找到第一个匹配项后可能继续搜索其他部分
  • 返回顺序不确定,但保证正确性

4. 变换算法

cpp 复制代码
std::for_each(std::execution::par, begin, end, function);
std::for_each_n(std::execution::par, begin, n, function);
std::transform(std::execution::par, begin1, end1, result, unary_op);
std::transform(std::execution::par, begin1, end1, begin2, result, binary_op);

含义:并行处理每个元素。

注意事项

  • 函数对象必须是线程安全的
  • 元素处理顺序不确定
  • 避免数据竞争

5. 复制/移动算法

cpp 复制代码
std::copy(std::execution::par, begin, end, result);
std::copy_n(std::execution::par, begin, n, result);
std::copy_if(std::execution::par, begin, end, result, predicate);
std::move(std::execution::par, begin, end, result);

注意事项

  • 源和目标范围不能重叠(除非是开头)
  • 并行复制可能提高大内存块传输速度

6. 填充算法

cpp 复制代码
std::fill(std::execution::par, begin, end, value);
std::fill_n(std::execution::par, begin, n, value);
std::generate(std::execution::par, begin, end, generator);
std::generate_n(std::execution::par, begin, n, generator);

注意事项

  • 生成器函数必须是线程安全的
  • 调用顺序不确定

7. 替换算法

cpp 复制代码
std::replace(std::execution::par, begin, end, old_val, new_val);
std::replace_if(std::execution::par, begin, end, predicate, new_val);
std::replace_copy(std::execution::par, begin, end, result, old_val, new_val);
std::replace_copy_if(std::execution::par, begin, end, result, predicate, new_val);

8. 移除算法

cpp 复制代码
std::remove(std::execution::par, begin, end, value);
std::remove_if(std::execution::par, begin, end, predicate);
std::remove_copy(std::execution::par, begin, end, result, value);
std::remove_copy_if(std::execution::par, begin, end, result, predicate);
std::unique(std::execution::par, begin, end);
std::unique_copy(std::execution::par, begin, end, result);

注意事项

  • 相对顺序可能改变
  • 通常需要配合erase使用

9. 划分算法

cpp 复制代码
std::partition(std::execution::par, begin, end, predicate);
std::partition_copy(std::execution::par, begin, end, out_true, out_false, predicate);
std::is_partitioned(std::execution::par, begin, end, predicate);

注意事项

  • 不保证稳定划分(相等元素相对顺序可能改变)

10. 合并算法

cpp 复制代码
std::merge(std::execution::par, begin1, end1, begin2, end2, result);

11. 集合算法

cpp 复制代码
std::includes(std::execution::par, begin1, end1, begin2, end2);
std::set_union(std::execution::par, begin1, end1, begin2, end2, result);
std::set_intersection(std::execution::par, begin1, end1, begin2, end2, result);
std::set_difference(std::execution::par, begin1, end1, begin2, end2, result);
std::set_symmetric_difference(std::execution::par, begin1, end1, begin2, end2, result);

12. 堆操作

cpp 复制代码
std::is_heap(std::execution::par, begin, end);
std::is_heap_until(std::execution::par, begin, end);

13. 最值算法

cpp 复制代码
std::min_element(std::execution::par, begin, end);
std::max_element(std::execution::par, begin, end);
std::minmax_element(std::execution::par, begin, end);

14. 其他算法

cpp 复制代码
std::reverse(std::execution::par, begin, end);
std::reverse_copy(std::execution::par, begin, end, result);
std::rotate(std::execution::par, begin, middle, end);
std::rotate_copy(std::execution::par, begin, middle, end, result);
std::is_sorted(std::execution::par, begin, end);
std::is_sorted_until(std::execution::par, begin, end);

2、重要注意事项

1. 线程安全性要求

cpp 复制代码
// 错误示例:非线程安全的函数对象
int counter = 0;
std::for_each(std::execution::par, v.begin(), v.end(),
    [&](auto& x) { ++counter; }); // 数据竞争!

// 正确做法:使用原子操作或避免共享状态
std::atomic<int> atomic_counter{0};
std::for_each(std::execution::par, v.begin(), v.end(),
    [&](auto& x) { ++atomic_counter; });

2. 异常处理

cpp 复制代码
try {
    std::sort(std::execution::par, v.begin(), v.end());
} catch(...) {
    // 并行算法可能抛出多个异常
    // 实际可能调用std::terminate()
}

3. 性能考虑

  • 数据规模:小数据可能因并行开销而变慢
  • 内存局部性:并行可能破坏缓存友好性
  • 负载均衡:确保任务分配均匀

4. 算法特定要求

cpp 复制代码
// reduce要求操作满足结合律
std::reduce(std::execution::par, v.begin(), v.end(), 0.0,
    [](double a, double b) { return a + b; }); // 浮点数不严格满足结合律

5. 执行策略选择指南

cpp 复制代码
// 小数据集 - 顺序执行
std::sort(std::execution::seq, small.begin(), small.end());

// 计算密集型 - 并行执行
std::transform(std::execution::par, large.begin(), large.end(), 
               result.begin(), complex_operation);

// SIMD友好操作 - 并行+向量化
std::for_each(std::execution::par_unseq, data.begin(), data.end(),
    [](auto& x) { x *= 2; });

并行算法是C++17的重要特性,正确使用可以显著提升性能,但需要仔细考虑线程安全和性能影响。

相关推荐
cpp_25017 小时前
P1583 魔法照片
数据结构·c++·算法·题解·洛谷
fpcc7 小时前
跟我学C++中级篇——constinit避免SIOF
c++
无限进步_7 小时前
【C语言】堆排序:从堆构建到高效排序的完整解析
c语言·开发语言·数据结构·c++·后端·算法·visual studio
雾岛听蓝7 小时前
STL 容器适配器:stack、queue 与 priority_queue
开发语言·c++
CSDN_RTKLIB7 小时前
【One Definition Rule】多编译单元定义同名全局变量
开发语言·c++
oioihoii8 小时前
实验报告:static变量与#include机制的相互作
c++
水饺编程9 小时前
下载和编译 VirtuaNES 模拟器源代码
c语言·c++·windows·visual studio
Fcy6489 小时前
AVL树(C++详解版)
开发语言·c++·avl树
张健115640964810 小时前
explicit和initializer_list
开发语言·c++
GetcharZp10 小时前
C++ 程序员一定要会的 RPC 框架:gRPC 从原理到实战,一次写通服务端和客户端
c++·后端·grpc