Async++ 源码分析7--parallel_reduce.h

一、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_reduceparallel_reduce 并行算法 的核心实现,位于 async:: 及其内部的 async::detail:: 命名空间,核心功能是对一个范围的元素先执行 "映射(Map)" 操作,再将映射结果 "归约(Reduce)" 为单个值,并通过多线程并行加速整个过程。以下是分层解析:

2.2. 核心概念铺垫

parallel_map_reduce 是 "Map-Reduce" 编程模型的并行实现,这一模型由两阶段组成,适用于 "大规模数据处理并聚合结果" 的场景(例如统计数组中所有元素的平方和、计算文件中所有单词的出现次数):

  • Map(映射)阶段:对范围中的每个元素执行相同的转换操作(如 "计算平方""提取单词"),生成中间结果;
  • Reduce(归约)阶段 :将所有中间结果聚合为单个最终值(如 "求和""统计次数"),聚合过程需满足结合律(确保并行拆分后结果一致)。

parallel_reduceparallel_map_reduce 的简化版 ------ 省略 "映射" 阶段(使用 "恒等映射",即元素直接作为中间结果),仅执行 "归约" 操作(如直接对数组元素求和、求最大值)。

2 .3. 代码结构与核心组件

代码分为 内部实现(detail::default_mapdetail::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 用于减少拷贝:归约过程中,outmapped 均通过移动语义传递,避免中间结果的拷贝开销(尤其适合大对象)。
步骤 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_reduceparallel_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)初始值的选择

初始值需与归约函数匹配,确保 "空范围时返回初始值" 且 "不影响非空范围的结果":

  • 求和:初始值 00 + a = a);
  • 求积:初始值 11 * a = a);
  • 取最大:初始值 INT_MINmax(INT_MIN, a) = a)。
(3)线程安全与数据竞争

映射函数 map 若操作共享数据(如全局变量),需自行保证线程安全;归约函数 reduce 仅处理局部结果,无数据竞争(每个归约操作的输入都是独立的局部结果)。

3. 总结

Async++ 的 parallel_map_reduce/parallel_reduce 通过 "递归拆分范围 + 并行归约 + Map-Reduce 融合" 实现了高效的并行数据处理:

  • 核心优势:内存开销低(无中间结果存储)、负载均衡(二分拆分)、接口灵活(支持自定义调度器 / 映射 / 归约);
  • 适用场景:大规模数据转换与聚合(如统计、求和、求最值);
  • 关系:parallel_reduceparallel_map_reduce 的简化版,通过 default_map 省略映射阶段,避免代码重复。

这两个算法是并行数据处理的核心工具,也是 Async++ 框架对 "分治思想" 的典型应用。

相关推荐
江公望5 小时前
Qt QThread使用方法入门浅解
c++·qt
叫我龙翔5 小时前
【MySQL】从零开始了解数据库开发 --- 数据表的约束
android·c++·mysql·数据库开发
量化交易曾小健(金融号)5 小时前
Python美股量化交易填坑记录——3.盈透(Interactive Brokers)证券API接口
开发语言·python
Yupureki5 小时前
从零开始的C++学习生活 6:string的入门使用
c语言·c++·学习·visual studio
Madison-No75 小时前
【C++】探秘string的底层实现
开发语言·c++
无限进步_6 小时前
C语言字符串与内存操作函数完全指南
c语言·c++·算法
lly2024066 小时前
AJAX JSON 实例
开发语言
闻缺陷则喜何志丹6 小时前
【C++贪心】P10537 [APIO2024] 九月|普及+
c++·算法·贪心·洛谷
QiZhang | UESTC6 小时前
JAVA算法练习题day27
java·开发语言·c++·算法·leetcode·hot100