3. 并行归并排序

并行归并排序 学习卡片

任务说明

实现一个并行归并排序程序:将输入数组递归分割为多个子数组,各子数组并行执行排序,最后合并为完整有序序列。通过改变并行线程数,观察并分析加速比变化。本案例使用C++标准线程库,不依赖外部并行框架。

并行归并排序实现

// parallel_merge_sort.cpp
#include <iostream>
#include <vector>
#include <thread>
#include <algorithm>
#include <chrono>
#include <random>

// 串行归并排序(用于基础比较和递归基)
void merge_sort_serial(std::vector<int>& arr, int left, int right) {
    if (left >= right) return;
    int mid = left + (right - left) / 2;
    merge_sort_serial(arr, left, mid);
    merge_sort_serial(arr, mid + 1, right);
    
    // 合并两个有序区间
    std::vector<int> temp(right - left + 1);
    int i = left, j = mid + 1, k = 0;
    while (i <= mid && j <= right) {
        if (arr[i] <= arr[j]) temp[k++] = arr[i++];
        else temp[k++] = arr[j++];
    }
    while (i <= mid) temp[k++] = arr[i++];
    while (j <= right) temp[k++] = arr[j++];
    for (int p = 0; p < temp.size(); ++p) arr[left + p] = temp[p];
}

// 并行归并排序
void merge_sort_parallel(std::vector<int>& arr, int left, int right, int depth) {
    if (left >= right) return;
    if (depth <= 0) {  // 达到深度限制,转串行
        merge_sort_serial(arr, left, right);
        return;
    }
    
    int mid = left + (right - left) / 2;
    
    // 创建两个子任务并行执行
    std::thread t1([&]() { merge_sort_parallel(arr, left, mid, depth - 1); });
    std::thread t2([&]() { merge_sort_parallel(arr, mid + 1, right, depth - 1); });
    t1.join();
    t2.join();
    
    // 合并(与串行版本相同)
    std::vector<int> temp(right - left + 1);
    int i = left, j = mid + 1, k = 0;
    while (i <= mid && j <= right) {
        if (arr[i] <= arr[j]) temp[k++] = arr[i++];
        else temp[k++] = arr[j++];
    }
    while (i <= mid) temp[k++] = arr[i++];
    while (j <= right) temp[k++] = arr[j++];
    for (int p = 0; p < temp.size(); ++p) arr[left + p] = temp[p];
}

// 验证排序正确性
bool verify_sorted(const std::vector<int>& arr) {
    for (size_t i = 1; i < arr.size(); ++i) {
        if (arr[i] < arr[i - 1]) return false;
    }
    return true;
}

int main(int argc, char* argv[]) {
    const int N = (argc > 1) ? std::stoi(argv[1]) : 1000000;  // 数组大小
    const int MAX_THREADS = (argc > 2) ? std::stoi(argv[2]) : 4;  // 最大线程数
    
    // 生成随机数据
    std::vector<int> data(N);
    std::mt19937 rng(42);
    std::uniform_int_distribution<int> dist(0, N * 10);
    for (int& x : data) x = dist(rng);
    
    // 串行排序计时
    std::vector<int> arr_serial = data;
    auto t0 = std::chrono::high_resolution_clock::now();
    merge_sort_serial(arr_serial, 0, N - 1);
    auto t1 = std::chrono::high_resolution_clock::now();
    double serial_time = std::chrono::duration<double>(t1 - t0).count();
    
    std::cout << "串行排序时间: " << serial_time << " 秒\n";
    std::cout << "串行排序正确: " << (verify_sorted(arr_serial) ? "是" : "否") << "\n";
    
    // 并行排序(不同线程数)
    for (int threads = 2; threads <= MAX_THREADS; threads *= 2) {
        std::vector<int> arr_par = data;
        int depth = 0;
        int temp = threads;
        while (temp > 1) { depth++; temp /= 2; }  // 计算递归深度
        
        auto t2 = std::chrono::high_resolution_clock::now();
        merge_sort_parallel(arr_par, 0, N - 1, depth);
        auto t3 = std::chrono::high_resolution_clock::now();
        double par_time = std::chrono::duration<double>(t3 - t2).count();
        
        std::cout << "\n线程数 " << threads << ":\n";
        std::cout << "  并行排序时间: " << par_time << " 秒\n";
        std::cout << "  排序正确: " << (verify_sorted(arr_par) ? "是" : "否") << "\n";
        std::cout << "  加速比: " << (serial_time / par_time) << "\n";
    }
    
    return 0;
}

编译与运行

g++ -O3 -std=c++11 -pthread parallel_merge_sort.cpp -o parallel_merge_sort
./parallel_merge_sort 1000000 4

并行策略说明

程序通过递归深度控制并行度:深度为0时转为串行排序,避免线程过多导致开销过大。每个递归层级创建两个线程分别处理左右子数组,线程数按2的幂次增长。合并阶段仍由父线程完成,保证数据一致性。

验证方法

运行程序后观察输出:

  • 串行与各并行配置的排序结果均显示“正确: 是”,证明排序功能无误;
  • 比较不同线程数下的排序时间,计算加速比(串行时间/并行时间);
  • 在4核及以上CPU上,预期2线程加速比接近1.5~1.8,4线程接近2.5~3.5(受内存带宽和合并开销限制,无法达到线性加速);
  • 若加速比小于1,说明数据量过小或线程开销占主导,可增大数组规模重新测试。
并行归并排序执行流程与线程分配 原始数组(N个元素) 线程1: 左半区 线程2: 右半区 线程3 线程4 线程5 线程6 … 继续递归分割,直到达到深度限制转为串行排序 … 自底向上合并有序子数组(合并操作由父线程完成) 合并后的有序左半区 合并后的有序右半区 最终完全有序数组 输入数据 并行任务 子任务 合并结果

加速比分析要点

加速比受以下因素影响:

  • 数据规模:N越大,并行计算占比越高,加速比越明显;
  • 线程开销:线程创建与上下文切换有固定成本,小数据量时可能抵消并行收益;
  • 合并瓶颈:合并阶段为串行操作,是并行归并排序的固有瓶颈;
  • 内存带宽:多线程同时访问内存可能造成带宽竞争,限制扩展性。

建议测试N=10⁶和N=10⁷两组数据,对比加速比变化趋势。若机器核心数较多,可尝试8线程观察是否继续提升。