C++并发编程实战:并行算法设计详解

C++并发编程实战:并行算法设计详解

【免费下载链接】Cpp_Concurrency_In_Action 【免费下载链接】Cpp_Concurrency_In_Action 项目地址: https://gitcode.com/gh_mirrors/cp/Cpp_Concurrency_In_Action

本文将深入探讨《C++并发编程实战》中关于并行算法设计的核心内容,重点分析三种标准库算法的并行实现:std::for_eachstd::findstd::partial_sum

并行算法设计基础

在设计并行算法时,我们需要考虑几个关键因素:

  1. 任务划分:如何将工作合理分配到多个线程
  2. 数据依赖性:任务之间是否存在依赖关系
  3. 异常处理:如何传播和处理线程中的异常
  4. 性能优化:避免伪共享、减少同步开销等

并行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(&parallel_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. 第一次计算相邻元素(距离为1)的和
  2. 然后计算距离为2的元素和
  3. 接着是距离为4的元素和
  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;
        }
    }
};

关键点

  • 分块实现适合处理器数量较少的情况
  • 基于栅栏的实现适合大规模并行系统
  • 栅栏确保所有线程同步前进
  • 需要特别注意异常处理和线程安全

总结

设计并行算法时,我们需要根据具体问题和硬件环境选择最合适的策略:

  1. 独立操作:如std::for_each最容易并行化
  2. 提前终止:如std::find需要额外的同步机制
  3. 数据依赖:如std::partial_sum需要更复杂的算法设计

理解这些并行算法的实现原理,可以帮助我们更好地设计自己的并发代码,充分利用现代多核处理器的计算能力。

【免费下载链接】Cpp_Concurrency_In_Action 【免费下载链接】Cpp_Concurrency_In_Action 项目地址: https://gitcode.com/gh_mirrors/cp/Cpp_Concurrency_In_Action

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值