C++并发编程实战:并行算法设计详解
【免费下载链接】Cpp_Concurrency_In_Action 项目地址: https://gitcode.com/gh_mirrors/cp/Cpp_Concurrency_In_Action
本文将深入探讨《C++并发编程实战》中关于并行算法设计的核心内容,重点分析三种标准库算法的并行实现:std::for_each、std::find和std::partial_sum。
并行算法设计基础
在设计并行算法时,我们需要考虑几个关键因素:
- 任务划分:如何将工作合理分配到多个线程
- 数据依赖性:任务之间是否存在依赖关系
- 异常处理:如何传播和处理线程中的异常
- 性能优化:避免伪共享、减少同步开销等
并行std::for_each实现
std::for_each是最容易并行化的算法之一,因为它对每个元素的操作是独立的。
实现方案一:显式线程管理
template<typename Iterator, typename Func>
void parallel_for_each(Iterator first, Iterator last, Func f) {
// 计算元素总数
unsigned long const length = std::distance(first, last);
if(!length) return;
// 计算线程数量
unsigned long const min_per_thread = 25;
unsigned long const max_threads = (length + min_per_thread - 1) / min_per_thread;
unsigned long const hardware_threads = std::thread::hardware_concurrency();
unsigned long const num_threads = std::min(hardware_threads != 0 ? hardware_threads : 2, max_threads);
// 计算每个线程处理的元素数量
unsigned long const block_size = length / num_threads;
// 创建线程和future
std::vector<std::future<void>> futures(num_threads-1);
std::vector<std::thread> threads(num_threads-1);
// 启动线程
Iterator block_start = first;
for(unsigned long i = 0; i < (num_threads-1); ++i) {
Iterator block_end = block_start;
std::advance(block_end, block_size);
std::packaged_task<void(void)> task([=]() {
std::for_each(block_start, block_end, f);
});
futures[i] = task.get_future();
threads[i] = std::thread(std::move(task));
block_start = block_end;
}
// 主线程处理剩余元素
std::for_each(block_start, last, f);
// 等待所有线程完成
for(unsigned long i = 0; i < (num_threads-1); ++i) {
futures[i].get(); // 获取结果以检查异常
}
}
实现方案二:使用std::async
template<typename Iterator, typename Func>
void parallel_for_each(Iterator first, Iterator last, Func f) {
unsigned long const length = std::distance(first, last);
if(!length) return;
unsigned long const min_per_thread = 25;
if(length < (2 * min_per_thread)) {
std::for_each(first, last, f);
} else {
Iterator const mid_point = first + length / 2;
std::future<void> first_half =
std::async(¶llel_for_each<Iterator, Func>, first, mid_point, f);
parallel_for_each(mid_point, last, f);
first_half.get();
}
}
关键点:
- 两种实现都能正确处理异常
- std::async版本更简洁,但可能产生更多线程
- 显式线程管理版本可以更好地控制线程数量
并行std::find实现
std::find的并行实现更为复杂,因为一旦找到匹配元素,我们希望立即停止所有线程的搜索。
实现方案
template<typename Iterator, typename MatchType>
Iterator parallel_find(Iterator first, Iterator last, MatchType match) {
struct find_element {
void operator()(Iterator begin, Iterator end,
MatchType match,
std::promise<Iterator>* result,
std::atomic<bool>* done_flag) {
try {
for(; (begin != end) && !done_flag->load(); ++begin) {
if(*begin == match) {
result->set_value(begin);
done_flag->store(true);
return;
}
}
} catch(...) {
try {
result->set_exception(std::current_exception());
done_flag->store(true);
} catch(...) {}
}
}
};
// 线程管理代码...
// 类似parallel_for_each的实现,但使用promise/future传递结果
}
关键点:
- 使用原子bool标志
done_flag来通知其他线程停止搜索 - 使用
std::promise传递找到的结果或异常 - 一旦某个线程找到匹配项,所有线程都会快速终止
并行std::partial_sum实现
std::partial_sum是最具挑战性的并行算法,因为元素之间存在数据依赖性。
分块实现方案
template<typename Iterator>
void parallel_partial_sum(Iterator first, Iterator last) {
typedef typename Iterator::value_type value_type;
struct process_chunk {
void operator()(Iterator begin, Iterator last,
std::future<value_type>* previous_end_value,
std::promise<value_type>* end_value) {
// 处理当前块的部分和
// 然后加上前一块的最后一个值
}
};
// 线程管理代码...
// 将数据分块,每个线程处理一块
// 前一块的结果传递给后一块
}
基于栅栏的实现方案
对于大规模并行系统,可以采用更复杂的算法:
- 第一次计算相邻元素(距离为1)的和
- 然后计算距离为2的元素和
- 接着是距离为4的元素和
- 依此类推,直到覆盖整个范围
这种算法需要实现栅栏同步:
class barrier {
unsigned const count;
std::atomic<unsigned> spaces;
std::atomic<unsigned> generation;
public:
explicit barrier(unsigned count_) :
count(count_), spaces(count), generation(0) {}
void wait() {
unsigned const my_generation = generation;
if(!--spaces) {
spaces = count.load();
++generation;
} else {
while(generation == my_generation)
std::this_thread::yield();
}
}
void done_waiting() {
--count;
if(!--spaces) {
spaces = count.load();
++generation;
}
}
};
关键点:
- 分块实现适合处理器数量较少的情况
- 基于栅栏的实现适合大规模并行系统
- 栅栏确保所有线程同步前进
- 需要特别注意异常处理和线程安全
总结
设计并行算法时,我们需要根据具体问题和硬件环境选择最合适的策略:
- 独立操作:如
std::for_each最容易并行化 - 提前终止:如
std::find需要额外的同步机制 - 数据依赖:如
std::partial_sum需要更复杂的算法设计
理解这些并行算法的实现原理,可以帮助我们更好地设计自己的并发代码,充分利用现代多核处理器的计算能力。
【免费下载链接】Cpp_Concurrency_In_Action 项目地址: https://gitcode.com/gh_mirrors/cp/Cpp_Concurrency_In_Action
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



