OpenMP 共享内存并行编程:从循环到任务

深入解析 OpenMP 并行构造、调度策略、同步原语与任务并行,配合 C 代码示例与性能实验对比。

OpenMP 是最简便的共享内存多线程并行接口。区别于 MPI 的显式消息传递,OpenMP 通过编译器指令在串行代码基础上标记可并行区域。对于 NUMA 架构的多核 CPU,正确使用 OpenMP 可以充分释放硬件并行潜力。

基本结构:parallel 与 for

#pragma omp parallel 声明一个并行区域:

#include <omp.h>
#include <stdio.h>

int main() {
    #pragma omp parallel
    {
        int tid = omp_get_thread_num();
        int nthreads = omp_get_num_threads();
        printf("Hello from thread %d of %d\n", tid, nthreads);
    }
    return 0;
}

编译:gcc -fopenmp hello.c -o hello

并行循环是 OpenMP 最常见的使用场景。#pragma omp parallel for 将外层循环自动分发给多个线程:

#pragma omp parallel for
for (int i = 0; i < N; i++) {
    c[i] = a[i] + b[i];
}

默认情况下,每个线程独立执行分配到的迭代范围。使用 default(none) 可强制显式声明每个变量的数据属性,避免未预期的数据竞争:

#pragma omp parallel for default(none) shared(a, b, c, N) private(i)
for (i = 0; i < N; i++) {
    c[i] = a[i] + b[i];
}

sections 多段任务并行

当几个独立的代码块需要并行执行时,使用 sections

#pragma omp parallel sections
{
    #pragma omp section
    { compute_pressure(grid); }
    #pragma omp section
    { compute_temperature(grid); }
    #pragma omp section
    { compute_velocity(grid); }
}

三个 section 由不同线程执行,隐式 barrier 等待全部完成。

reduction 子句

并行求和或累乘时,reduction 自动处理线程私有累加器和最终归约:

double sum = 0.0;
#pragma omp parallel for reduction(+:sum)
for (int i = 0; i < N; i++) {
    sum += a[i] * b[i];
}
// 所有线程的局部 sum 被自动加总到共享变量 sum
操作符reduction 用途
+求和、点积
*连乘、概率累积
max / min极值搜索
& / `/^`
&& / `
double max_val = -INFINITY;
#pragma omp parallel for reduction(max:max_val)
for (int i = 0; i < N; i++) {
    if (a[i] > max_val) max_val = a[i];
}

OpenMP 自动实现树形归约,避免串行累加瓶颈。

schedule 调度策略详解

循环迭代分配到线程粒度直接影响性能,OpenMP 提供四种调度方式:

#pragma omp parallel for schedule(static, 16)
for (int i = 0; i < N; i++) { ... }
策略机制适用场景开销负载均衡
static预先均等分配迭代块各迭代计算量均匀,矩阵计算最低均匀负载时最优
dynamic线程动态获取小任务块迭代计算量差异大,蒙特卡洛较高
guided任务块大小指数递减早期迭代开销大,递归问题较好
runtime由环境变量 OMP_SCHEDULE 控制运行时灵活调整依赖外部依赖设置

参数 chunk 指定每次分配多少迭代。对于静态负载,增大 chunk 减少调度开销;对于动态负载,小块确保及时负载均衡。

N=64, 4 threads, schedule(static, 8):
Thread 0: [0-7]  [8-15]
Thread 1: [16-23] [24-31]
Thread 2: [32-39] [40-47]
Thread 3: [48-55] [56-63]

schedule(dynamic, 4): 线程空闲时动态申请 4 个迭代

调度性能实验对比(100M 次迭代,耗时毫秒):

策略均匀负载耗时随机不等量负载耗时
static45ms280ms
static + large chunk42ms310ms
dynamic, 102448ms95ms
guided46ms78ms

均匀负载选 static,负载波动选 guided 或动态小 chunk。

同步原语

barrier 显式路障

#pragma omp barrier 阻塞线程直到所有线程到达该点。隐式 barrier 出现在 forsectionssingle 区域末尾。

#pragma omp parallel
{
    phase_one();       // 阶段一计算
    #pragma omp barrier // 确保全部完成后再进入阶段二
    phase_two();       // 依赖阶段一结果
}

critical / atomic / ordered

critical:保证代码块串行执行:

double local_sum = 0.0;
#pragma omp parallel for reduction(+:local_sum)
for (...) { local_sum += ...; }

#pragma omp critical
{
    global_sum += local_sum;
    write_log("thread partial", local_sum);
}

atomic:更轻量的单操作原子化,无锁开销最低:

#pragma omp parallel for
for (int i = 0; i < N; i++) {
    if (a[i] > threshold) {
        #pragma omp atomic
        counter++;
    }
}

atomic 仅支持有限的运算符(+, -, *, /, &, |, ^, <<, >>)。

ordered:保持循环迭代的执行顺序,适用于输出依赖场景的调试:

#pragma omp parallel for ordered schedule(static)
for (int i = 0; i < N; i++) {
    double result = compute(i);
    #pragma omp ordered
    {
        output_buffer[i] = result;
    }
}

避免伪共享(False Sharing)

当多个线程写入同一缓存行(通常 64B)的不同位置时,缓存一致性协议导致性能暴跌:

// 错误:所有 counter 可能落在同一缓存行
int counters[64];  // 线程 i 写 counters[i]

// 正确:padding 确保每个 counter 独占缓存行
struct padded_counter {
    int val;
    char padding[60];  // 64 - sizeof(int)
} __attribute__((aligned(64)));
struct padded_counter counters[64];

task 任务并行

OpenMP 3.0 引入的 task 允许递归和动态划分不可预测的并行性:

void fib_task(int n, int *out) {
    if (n < 2) {
        *out = n;
        return;
    }
    int x, y;
    #pragma omp task shared(x)
    fib_task(n - 1, &x);
    #pragma omp task shared(y)
    fib_task(n - 2, &y);
    #pragma omp taskwait
    *out = x + y;
}

int main() {
    int result;
    #pragma omp parallel
    {
        #pragma omp single
        fib_task(40, &result);
    }
    printf("fib(40) = %d\n", result);
}

关键指令说明:

指令作用
task创建异步任务,可由任意线程执行
taskwait等待当前任务的所有子任务完成
single仅一个线程执行块内代码,隐式 barrier
master仅主线程执行,无隐式同步

task 模型特别适合图遍历、n-Queens、稀疏矩阵分解等递归分支结构。

环境变量速查

export OMP_NUM_THREADS=64          # 线程数
export OMP_PLACES=cores            # 绑定到物理核心
export OMP_PROC_BIND=close         # 线程在相邻核心上分布
export OMP_DYNAMIC=false           # 禁止运行时动态调整线程数
export OMP_NESTED=true             # 允许并行区域嵌套
export OMP_MAX_ACTIVE_LEVELS=2     # 最大嵌套深度

线程绑定对 NUMA 架构至关重要。OMP_PROC_BIND=spread 将线程均匀分散到各 NUMA 节点以最大化聚合带宽;close 则将线程集中在最小核心集以提高缓存利用率。

// 查看运行时绑定信息
#pragma omp parallel
{
    int cpuid = sched_getcpu();
    printf("Thread %d on CPU %d\n", omp_get_thread_num(), cpuid);
}

OpenMP 以极低的侵入性成本和成熟的编译器支持,成为多核 CPU 并发的首选工具。掌握 reduction、schedule 和 task 三大核心能力,足以应对绝大多数 HPC 和 AI 推理中的 CPU 并行需求。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「hpc」更多文章

  1. Slurm 集群调度系统深度解析与实战
  2. Roofline 性能模型:判定性能瓶颈与优化方向
  3. ROCm HIP GPU 编程实战