1、背景
async++框架中提供了多种并行计算的工具,其中包括parallel_for、parallel_invoke、parallel_reduce。这3中工具的使用场景略有不同,下面将对它们进行比较详细的介绍。
2、parallel_for
2.1、核心模板函数
// 这个函数是一个递归设计
// 为什么只限制了前半部分任务完成后才可以执行后半部分任务呢?
// 我理解是因为前半部分任务使用了异步方法,而后半部分和我们使用的平时的递归没有差别
// 因此只需要等待前半部分即可。
template<typename Sched, typename Partitioner, typename Func>
void internal_parallel_for(Sched& sched, Partitioner partitioner, const Func& func)
{
// Split the partition, run inline if no more splits are possible
// 将数据集分割
auto subpart = partitioner.split();
// 判断是不是到了退出递归的条件
if (subpart.begin() == subpart.end()) {
for (auto&& i: partitioner)
func(std::forward<decltype(i)>(i));
return;
}
// Run the function over each half in parallel
// 异步执行前半部分任务,这里已经开始执行任务
auto&& t = async::local_spawn(sched, [&sched, &subpart, &func] {
detail::internal_parallel_for(sched, std::move(subpart), func);
});
// 执行后办部分任务
detail::internal_parallel_for(sched, std::move(partitioner), func);
// 为什么只需等待前半部分任务结束,请看模板函数上面的注释
t.get();
}
2.2、功能及场景介绍
internal_parallel_for 是一种并行化的循环执行函数,适用于在多个线程中并行执行对容器中每个元素应用相同操作的场景。每个线程处理容器的不同部分,执行相同的操作,典型的使用场景是遍历数组或集合。关键点如下:
- 数据驱动:它的任务是对一个数据集(由 partitioner 提供)执行相同的操作,每个数据项执行相同的函数 func。
- 任务拆分与并行:它会将数据分割成多个部分,并在多个线程中并行处理这些部分。如果无法再拆分,任务会在当前线程中顺序执行。
- 没有归约操作:所有的任务是相互独立的,因此没有跨任务的结果合并操作。每个任务的执行不会影响其他任务。
适合于执行大规模的、每个元素独立的操作,例如遍历数组或容器,并对每个元素执行某个计算。常见的应用包括图像处理、矩阵运算等。
3、parallel_invoke
3.1、核心模板函数
// 这段代码的目的是实现一个将多个函数并行执行的框架。
// 它通过递归的方式将任务拆分成更小的子任务,然后在多个线程中并行执行。
// 主要逻辑通过 parallel_invoke_internal 模板结构体来实现,递归地分割任务,并使用调度器来异步执行每个任务。
template<std::size_t Start, std::size_t Count>
struct parallel_invoke_internal {
template<typename Sched, typename Tuple>
static void run(Sched& sched, const Tuple& args)
{
auto&& t = async::local_spawn(sched, [&sched, &args] {
parallel_invoke_internal<Start + Count / 2, Count - Count / 2>::run(sched, args);
});
parallel_invoke_internal<Start, Count / 2>::run(sched, args);
t.get();
}
};
// parallel_invoke_internal 特化版本(处理单个任务)
template<std::size_t Index>
struct parallel_invoke_internal<Index, 1> {
template<typename Sched, typename Tuple>
static void run(Sched&, const Tuple& args)
{
// Make sure to preserve the rvalue/lvalue-ness of the original parameter
// std::get<Index>(args) 获取元组中的第 Index 个元素,并通过 std::forward 完美转发这个元素,
// 以保证它的值类别(左值或右值)不丢失,然后执行该元素(假设它是一个可调用对象)
std::forward<typename std::tuple_element<Index, Tuple>::type>(std::get<Index>(args))();
}
};
//parallel_invoke_internal 特化版本(处理空任务)
template<std::size_t Index>
struct parallel_invoke_internal<Index, 0> {
template<typename Sched, typename Tuple>
static void run(Sched&, const Tuple&) {}
};
3.2、功能及场景介绍
parallel_invoke 是一种并行执行多个独立任务的方式,它不会依赖于数据,而是允许在多个线程中并行执行多个完全独立的任务。每个任务可以是不同的函数。关键特点如下:
- 任务驱动:它执行多个不同的任务,每个任务都可以是独立的,不需要共享数据或状态。
- 并行执行多个任务:任务会被拆分成多个子任务,在不同的线程中并行执行。每个任务是相互独立的,且每个线程执行的任务是不同的。
- 递归拆分任务:任务会被递归地拆分成更小的子任务,每个子任务执行一个不同的任务。
适合于并行执行多个独立的操作,这些操作不依赖于其他任务的结果。比如,你希望并行执行多个完全不同的函数任务,或者在不同线程中调用不同的函数。
4、parallel_reduce
4.1、核心模板函数
// Result 是最终结果的类型。
// MapFunc 是映射函数的类型,用于将输入元素映射到某个值,它是一个可以调用的对象
// ReduceFunc 是归约函数的类型,用于将多个值合并成一个结果。
// 它的核心功能是将数据划分为多个子任务,并对每个子任务执行映射(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)
{
// 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());
}
4.2、功能及场景介绍
internal_parallel_map_reduce 是 map-reduce 模式的并行实现,结合了数据映射和结果归约的操作。它不仅执行映射操作(类似 parallel_for),还需要对结果进行归约(合并)。关键特点:
- 数据映射与归约:首先,对每个元素执行 map 操作,然后使用 reduce 函数合并这些结果。归约操作可以是求和、求最大值、连接等。
- 需要返回值:与 parallel_for 不同,internal_parallel_map_reduce 需要返回每个任务的结果,并将这些结果通过 reduce 归约成最终的结果。
- 任务拆分与并行执行:与 parallel_for 类似,任务会被拆分并在多个线程中并行执行。不同之处在于,每个任务的结果都会返回并进行合并。
适合于需要并行执行 map 操作并将结果归约(如求和、求最大值等)的任务。例如,分布式计算中对大规模数据的统计、求平均数等。
5、parallel_for、parallel_invoke、parallel_reduce比较
- internal_parallel_for:并行化一个普通的循环,执行相同的操作,没有结果返回,适用于容器中每个元素的独立操作,每个任务的计算是相同的。
- parallel_invoke:并行执行多个独立的任务,每个任务可以是不同的函数,适用于执行多个不相关的函数操作。
- internal_parallel_map_reduce:并行化 map-reduce 操作,执行映射(map)操作并进行归约(reduce),它适用于需要对数据进行转换并聚合结果的场景。