一、Async++ 代码目录结构
Async++ 项目的目录结构清晰,主要包含根目录下的配置文件、源代码目录、头文件目录以及示例代码目录,具体结构如下:
asyncplusplus/
├── .gitignore # Git 忽略文件配置
├── Async++Config.cmake.in # CMake 配置模板文件
├── CMakeLists.txt # CMake 构建脚本
├── LICENSE # 许可证文件(MIT 许可证)
├── README.md # 项目说明文档
├── examples/ # 示例代码目录
│ └── gtk_scheduler.cpp # GTK 调度器示例
├── src/ # 源代码目录
│ ├── fifo_queue.h # FIFO 队列实现
│ ├── internal.h # 内部头文件(包含类型定义、宏等)
│ ├── scheduler.cpp # 调度器实现
│ ├── singleton.h # 单例模式实现
│ ├── task_wait_event.h # 任务等待事件实现
│ ├── threadpool_scheduler.cpp # 线程池调度器实现
│ └── work_steal_queue.h # 工作窃取队列实现
└── include/ # 头文件目录
├── async++.h # 主头文件(对外提供统一接口)
└── async++/ # 子模块头文件目录
├── aligned_alloc.h
├── cancel.h
├── continuation_vector.h
├── parallel_for.h
├── parallel_invoke.h
├── parallel_reduce.h
├── partitioner.h # 分区器相关定义
├── range.h # 范围(迭代器对)相关定义
├── ref_count.h
├── scheduler.h # 调度器接口定义
├── scheduler_fwd.h
├── task.h # 任务类定义
├── task_base.h # 任务基类定义
├── traits.h
└── when_all_any.h
二、parallel_reduce源码分析
2.1 源码
cpp
// Copyright (c) 2015 Amanieu d'Antras
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
#ifndef ASYNCXX_H_
# error "Do not include this header directly, include <async++.h> instead."
#endif
namespace async {
namespace detail {
// Default map function which simply passes its parameter through unmodified
struct default_map {
template<typename T>
T&& operator()(T&& x) const
{
return std::forward<T>(x);
}
};
// Internal implementation of parallel_map_reduce that only accepts a
// partitioner argument.
template<typename Sched, typename Partitioner, typename Result, typename MapFunc, typename ReduceFunc>
Result internal_parallel_map_reduce(Sched& sched, Partitioner partitioner, Result init, const MapFunc& map, const ReduceFunc& reduce)
{
// Split the partition, run inline if no more splits are possible
auto subpart = partitioner.split();
if (subpart.begin() == subpart.end()) {
Result out = init;
for (auto&& i: partitioner)
out = reduce(std::move(out), map(std::forward<decltype(i)>(i)));
return out;
}
// Run the function over each half in parallel
auto&& t = async::local_spawn(sched, [&sched, &subpart, init, &map, &reduce] {
return detail::internal_parallel_map_reduce(sched, std::move(subpart), init, map, reduce);
});
Result out = detail::internal_parallel_map_reduce(sched, std::move(partitioner), init, map, reduce);
return reduce(std::move(out), t.get());
}
} // namespace detail
// Run a function for each element in a range and then reduce the results of that function to a single value
template<typename Sched, typename Range, typename Result, typename MapFunc, typename ReduceFunc>
Result parallel_map_reduce(Sched& sched, Range&& range, Result init, const MapFunc& map, const ReduceFunc& reduce)
{
return detail::internal_parallel_map_reduce(sched, async::to_partitioner(std::forward<Range>(range)), init, map, reduce);
}
// Overload with default scheduler
template<typename Range, typename Result, typename MapFunc, typename ReduceFunc>
Result parallel_map_reduce(Range&& range, Result init, const MapFunc& map, const ReduceFunc& reduce)
{
return async::parallel_map_reduce(::async::default_scheduler(), range, init, map, reduce);
}
// Overloads with std::initializer_list
template<typename Sched, typename T, typename Result, typename MapFunc, typename ReduceFunc>
Result parallel_map_reduce(Sched& sched, std::initializer_list<T> range, Result init, const MapFunc& map, const ReduceFunc& reduce)
{
return async::parallel_map_reduce(sched, async::make_range(range.begin(), range.end()), init, map, reduce);
}
template<typename T, typename Result, typename MapFunc, typename ReduceFunc>
Result parallel_map_reduce(std::initializer_list<T> range, Result init, const MapFunc& map, const ReduceFunc& reduce)
{
return async::parallel_map_reduce(async::make_range(range.begin(), range.end()), init, map, reduce);
}
// Variant with identity map operation
template<typename Sched, typename Range, typename Result, typename ReduceFunc>
Result parallel_reduce(Sched& sched, Range&& range, Result init, const ReduceFunc& reduce)
{
return async::parallel_map_reduce(sched, range, init, detail::default_map(), reduce);
}
template<typename Range, typename Result, typename ReduceFunc>
Result parallel_reduce(Range&& range, Result init, const ReduceFunc& reduce)
{
return async::parallel_reduce(::async::default_scheduler(), range, init, reduce);
}
template<typename Sched, typename T, typename Result, typename ReduceFunc>
Result parallel_reduce(Sched& sched, std::initializer_list<T> range, Result init, const ReduceFunc& reduce)
{
return async::parallel_reduce(sched, async::make_range(range.begin(), range.end()), init, reduce);
}
template<typename T, typename Result, typename ReduceFunc>
Result parallel_reduce(std::initializer_list<T> range, Result init, const ReduceFunc& reduce)
{
return async::parallel_reduce(async::make_range(range.begin(), range.end()), init, reduce);
}
} // namespace async
这段代码是 Async++ 框架中 parallel_map_reduce
与 parallel_reduce
并行算法 的核心实现,位于 async::
及其内部的 async::detail::
命名空间,核心功能是对一个范围的元素先执行 "映射(Map)" 操作,再将映射结果 "归约(Reduce)" 为单个值,并通过多线程并行加速整个过程。以下是分层解析:
2.2. 核心概念铺垫
parallel_map_reduce
是 "Map-Reduce" 编程模型的并行实现,这一模型由两阶段组成,适用于 "大规模数据处理并聚合结果" 的场景(例如统计数组中所有元素的平方和、计算文件中所有单词的出现次数):
- Map(映射)阶段:对范围中的每个元素执行相同的转换操作(如 "计算平方""提取单词"),生成中间结果;
- Reduce(归约)阶段 :将所有中间结果聚合为单个最终值(如 "求和""统计次数"),聚合过程需满足结合律(确保并行拆分后结果一致)。
parallel_reduce
是 parallel_map_reduce
的简化版 ------ 省略 "映射" 阶段(使用 "恒等映射",即元素直接作为中间结果),仅执行 "归约" 操作(如直接对数组元素求和、求最大值)。
2 .3. 代码结构与核心组件
代码分为 内部实现(detail::default_map
与 detail::internal_parallel_map_reduce
) 和 外部接口(parallel_map_reduce
/parallel_reduce
函数重载) 两层,内部通过 "递归拆分范围 + 并行归约" 实现核心逻辑,外部通过重载适配调度器和范围类型。
(1)内部辅助组件:detail::default_map
恒等映射
这是一个空结构体,实现 "恒等映射" 逻辑 ------ 直接返回输入元素,不做任何转换,用于 parallel_reduce
中替代 "自定义映射函数":
cpp
struct default_map {
template<typename T>
T&& operator()(T&& x) const
{
return std::forward<T>(x); // 直接转发输入,无转换
}
};
作用 :当用户不需要 "映射" 阶段时(仅需归约),parallel_reduce
会将 default_map
传入 parallel_map_reduce
,避免代码重复实现。
(2)内部核心逻辑:detail::internal_parallel_map_reduce
函数
这是 parallel_map_reduce
的核心实现,通过 递归拆分范围 + 并行归约 完成 "Map-Reduce" 流程,支持自定义调度器、分区器、映射函数和归约函数。
template<typename Sched, typename Partitioner, typename Result, typename MapFunc, typename ReduceFunc>
Result internal_parallel_map_reduce(Sched& sched, Partitioner partitioner, Result init, const MapFunc& map, const ReduceFunc& reduce)
参数说明:
参数名 | 作用 |
---|---|
Sched& sched |
调度器(用于分配子任务到线程池) |
Partitioner partitioner |
分区器(用于拆分遍历范围为子范围) |
Result init |
归约的初始值(如求和时的 0 、求积时的 1 ) |
MapFunc& map |
映射函数(对每个元素执行转换,输入为元素,输出为中间结果) |
ReduceFunc& reduce |
归约函数(对两个中间结果执行聚合,输入为两个值,输出为聚合后的值) |
核心逻辑(分两步:拆分范围 → 并行归约):
步骤 1:尝试拆分范围,判断是否终止递归(Map + 局部归约)
cpp
// 调用分区器的 split() 方法,将当前范围拆分为一个子范围(subpart)
auto subpart = partitioner.split();
// 若子范围为空(无法再拆分),则对当前范围执行"局部 Map+Reduce"
if (subpart.begin() == subpart.end()) {
Result out = init; // 局部归约的初始值
for (auto&& i: partitioner) {
// 1. Map 阶段:对当前元素执行映射函数,生成中间结果
auto mapped = map(std::forward<decltype(i)>(i));
// 2. 局部 Reduce 阶段:将中间结果聚合到 out 中
out = reduce(std::move(out), std::move(mapped));
}
return out; // 返回当前范围的局部归约结果
}
关键逻辑:
- 当范围无法再拆分(达到最小粒度,如单个元素)时,直接串行遍历该范围:先对每个元素执行
map
生成中间结果,再用reduce
将中间结果聚合为 "局部结果",避免过度拆分导致的线程开销; std::move
用于减少拷贝:归约过程中,out
和mapped
均通过移动语义传递,避免中间结果的拷贝开销(尤其适合大对象)。
步骤 2:并行处理两个子范围,聚合最终结果
cpp
// 1. 将"拆分出的子范围(subpart)"封装为异步任务,并行执行 Map-Reduce
auto&& t = async::local_spawn(sched, [&sched, &subpart, init, &map, &reduce] {
// 递归调用,对 subpart 执行 Map-Reduce,返回该子范围的归约结果
return detail::internal_parallel_map_reduce(sched, std::move(subpart), init, map, reduce);
});
// 2. 当前线程对"剩余范围(partitioner)"执行 Map-Reduce,返回局部结果
Result out = detail::internal_parallel_map_reduce(sched, std::move(partitioner), init, map, reduce);
// 3. 归约两个子范围的结果:将当前线程结果与异步任务结果聚合,返回最终值
return reduce(std::move(out), t.get());
并行逻辑:
- 采用 "二分法" 拆分范围:将当前范围拆分为
subpart
(异步执行)和partitioner
(当前线程执行),两个子范围并行执行 Map-Reduce; - 最终聚合:
t.get()
等待异步任务完成,获取其归约结果,再用reduce
函数将 "当前线程结果" 与 "异步任务结果" 聚合为最终值,确保所有子范围的结果都被合并; - 递归深度:每次拆分都会将范围减半,直到所有子范围都无法拆分,最终通过多线程并行加速整个流程。
(3)外部接口:parallel_map_reduce
与 parallel_reduce
重载
外部接口负责将 "用户传入的范围" 转换为 "分区器",并适配调度器参数,简化用户调用。
① parallel_map_reduce
重载
支持自定义调度器、范围类型(普通范围 /std::initializer_list
):
cpp
// 1. 自定义调度器 + 普通范围
template<typename Sched, typename Range, typename Result, typename MapFunc, typename ReduceFunc>
Result parallel_map_reduce(Sched& sched, Range&& range, Result init, const MapFunc& map, const ReduceFunc& reduce)
{
// 将范围转为分区器,调用内部实现
return detail::internal_parallel_map_reduce(sched, async::to_partitioner(std::forward<Range>(range)), init, map, reduce);
}
// 2. 默认调度器 + 普通范围
template<typename Range, typename Result, typename MapFunc, typename ReduceFunc>
Result parallel_map_reduce(Range&& range, Result init, const MapFunc& map, const ReduceFunc& reduce)
{
return async::parallel_map_reduce(::async::default_scheduler(), range, init, map, reduce);
}
// 3. 自定义调度器 + initializer_list(如 {1,2,3})
template<typename Sched, typename T, typename Result, typename MapFunc, typename ReduceFunc>
Result parallel_map_reduce(Sched& sched, std::initializer_list<T> range, Result init, const MapFunc& map, const ReduceFunc& reduce)
{
return async::parallel_map_reduce(sched, async::make_range(range.begin(), range.end()), init, map, reduce);
}
// 4. 默认调度器 + initializer_list
template<typename T, typename Result, typename MapFunc, typename ReduceFunc>
Result parallel_map_reduce(std::initializer_list<T> range, Result init, const MapFunc& map, const ReduceFunc& reduce)
{
return async::parallel_map_reduce(async::make_range(range.begin(), range.end()), init, map, reduce);
}
② parallel_reduce
重载
本质是调用 parallel_map_reduce
并传入 detail::default_map
(恒等映射),省略 "映射" 阶段:
cpp
// 1. 自定义调度器 + 普通范围
template<typename Sched, typename Range, typename Result, typename ReduceFunc>
Result parallel_reduce(Sched& sched, Range&& range, Result init, const ReduceFunc& reduce)
{
// 传入 default_map 作为映射函数
return async::parallel_map_reduce(sched, range, init, detail::default_map(), reduce);
}
// 2. 默认调度器 + 普通范围
template<typename Range, typename Result, typename ReduceFunc>
Result parallel_reduce(Range&& range, Result init, const ReduceFunc& reduce)
{
return async::parallel_reduce(::async::default_scheduler(), range, init, reduce);
}
// 3. 自定义调度器 + initializer_list
template<typename Sched, typename T, typename Result, typename ReduceFunc>
Result parallel_reduce(Sched& sched, std::initializer_list<T> range, Result init, const ReduceFunc& reduce)
{
return async::parallel_reduce(sched, async::make_range(range.begin(), range.end()), init, reduce);
}
// 4. 默认调度器 + initializer_list
template<typename T, typename Result, typename ReduceFunc>
Result parallel_reduce(std::initializer_list<T> range, Result init, const ReduceFunc& reduce)
{
return async::parallel_reduce(async::make_range(range.begin(), range.end()), init, reduce);
}
2.4. 关键设计亮点
(1)Map-Reduce 阶段融合,减少中间存储
与 "先批量执行 Map 生成所有中间结果,再执行 Reduce" 的 naive 实现不同,该代码在 "每个子范围" 中都将 Map 和局部 Reduce 融合:对元素执行 Map 后立即 Reduce 到局部结果,无需存储所有中间结果,大幅减少内存开销(尤其适合大规模数据)。
(2)递归二分拆分,确保负载均衡
通过 "每次拆分范围为两部分" 的策略,确保所有子范围的任务量尽可能均匀,避免某线程任务过多、某线程空闲的 "负载不均衡" 问题,充分利用 CPU 多核资源。
(3)归约结合律依赖与正确性保障
归约函数 reduce
需满足 结合律 (即 reduce(a, reduce(b, c)) = reduce(reduce(a, b), c)
),否则并行拆分后的结果可能与串行执行不一致。框架不强制检查结合律,需用户自行保证(例如 "求和""求积" 满足结合律,"减法""除法" 不满足)。
2.5. 用法示例
示例 1:parallel_map_reduce
计算数组元素的平方和
cpp
#include <async++.h>
#include <vector>
#include <iostream>
int main() {
std::vector<int> nums = {1, 2, 3, 4, 5};
// 初始值 0,映射函数:计算平方,归约函数:求和
int sum_of_squares = async::parallel_map_reduce(
nums,
0, // 归约初始值
[](int x) { return x * x; }, // Map:x → x²
[](int a, int b) { return a + b; } // Reduce:a + b
);
std::cout << "Sum of squares: " << sum_of_squares << "\n"; // 输出:1+4+9+16+25=55
return 0;
}
示例 2:parallel_reduce
计算数组元素的最大值
cpp
#include <async++.h>
#include <vector>
#include <iostream>
#include <algorithm>
int main() {
std::vector<int> nums = {3, 1, 4, 1, 5, 9};
// 初始值 INT_MIN,归约函数:取最大值(无映射,用默认恒等映射)
int max_val = async::parallel_reduce(
nums,
INT_MIN, // 归约初始值(确保小于所有元素)
[](int a, int b) { return std::max(a, b); } // Reduce:取最大值
);
std::cout << "Max value: " << max_val << "\n"; // 输出:9
return 0;
}
示例 3:自定义调度器与 initializer_list
cpp
#include <async++.h>
#include <iostream>
int main() {
// 创建包含 2 个线程的线程池调度器
async::threadpool_scheduler pool(2);
// 用 initializer_list 作为范围,计算元素的立方和
int sum_of_cubes = async::parallel_map_reduce(
pool, // 自定义调度器
{2, 3, 4}, // initializer_list 范围
0,
[](int x) { return x * x * x; }, // Map:x → x³
[](int a, int b) { return a + b; } // Reduce:求和
);
std::cout << "Sum of cubes: " << sum_of_cubes << "\n"; // 输出:8+27+64=99
return 0;
}
2.6. 注意事项
(1)归约函数的结合律
必须确保 reduce
函数满足结合律,否则并行执行结果可能与串行不一致。例如:
- 正确选择:求和(
a+b
)、求积(a*b
)、取最大(max(a,b)
); - 错误选择:减法(
a-b
)、除法(a/b
)(不满足结合律)。
(2)初始值的选择
初始值需与归约函数匹配,确保 "空范围时返回初始值" 且 "不影响非空范围的结果":
- 求和:初始值
0
(0 + a = a
); - 求积:初始值
1
(1 * a = a
); - 取最大:初始值
INT_MIN
(max(INT_MIN, a) = a
)。
(3)线程安全与数据竞争
映射函数 map
若操作共享数据(如全局变量),需自行保证线程安全;归约函数 reduce
仅处理局部结果,无数据竞争(每个归约操作的输入都是独立的局部结果)。
3. 总结
Async++ 的 parallel_map_reduce
/parallel_reduce
通过 "递归拆分范围 + 并行归约 + Map-Reduce 融合" 实现了高效的并行数据处理:
- 核心优势:内存开销低(无中间结果存储)、负载均衡(二分拆分)、接口灵活(支持自定义调度器 / 映射 / 归约);
- 适用场景:大规模数据转换与聚合(如统计、求和、求最值);
- 关系:
parallel_reduce
是parallel_map_reduce
的简化版,通过default_map
省略映射阶段,避免代码重复。
这两个算法是并行数据处理的核心工具,也是 Async++ 框架对 "分治思想" 的典型应用。