Introduction HPC

Sequential programs(串行程序)

Parallel programs(并行程序)

OpenMP

OpenMP execution model

fork - join 并行模式

只有一个master thread ,程序开始(单线程)- fork - parallel - join

OpenMP memory model

All threads have access to the same **shared memory space **所有线程共享同意内存空间

OpenMP derectives

1
#pragma omp <directive> [clause[[,] clause] ... ]
  • #pragma omp :告诉编译器这是一个 OpenMP 指令。
  • :具体的并行控制指令。
  • clause :附加选项(子句),比如 private、reduction、schedule。

指令

parallel

表是这段代码被多个线程并行执行

1
2
3
4
#pragma omp parallel
{
printf("Hello world \n");
}

for

用于for循环之前,将循环分配到多个线程执行,必须保证每次循环之间无相关性!

1
2
3
4
5
6
7
#pragma omp parallel   // 创建一个线程组
{
#pragma omp for // 把 for 循环的迭代分配给这些线程
for (int i = 0; i < N; i++) {
A[i] = i * 2;
}
}

parallel for

等价于 parallel + for

1
2
3
4
#pragma omp parallel for
for (int i = 0; i < N; i++) {
A[i] = i * 2;
}

sections

把不同的代码段(section)分配给不同的线程去执行,每个 section 只会执行一次。

相当于 任务并行(task parallelism),而不是 数据并行(data parallelism)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>
#include <omp.h>

int main() {
#pragma omp parallel
{
#pragma omp sections
{
#pragma omp section
{
printf("Thread %d doing task A\n", omp_get_thread_num());
}

#pragma omp section
{
printf("Thread %d doing task B\n", omp_get_thread_num());
}

#pragma omp section
{
printf("Thread %d doing task C\n", omp_get_thread_num());
}
}
}
return 0;
}

parallel sections

single

在一个并行区域里,只允许一个线程执行这段代码,而其他线程 跳过它,继续往下执行

1
2
3
4
5
6
7
#pragma omp parallel
{
#pragma omp single
{
printf("Only one thread executes this!\n");
}
}

barrier

让所有线程在这里集合(同步点),只有当 所有线程都到达这个位置 时,才会一起继续往下执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
#include <omp.h>

int main() {
#pragma omp parallel
{
int tid = omp_get_thread_num();

printf("Thread %d: part A done\n", tid);

#pragma omp barrier // 所有线程必须等到这里

printf("Thread %d: part B done\n", tid);
}
return 0;
}

Thread 0: part A done
Thread 2: part A done
Thread 1: part A done
Thread 3: part A done
Thread 0: part B done
Thread 1: part B done
Thread 2: part B done
Thread 3: part B done
  • part A 部分的输出是乱序的(线程独立执行)。
  • 但是所有线程必须 等到 barrier,再一起进入 part B
  • 所以 part B 的内容永远不会早于任何一个 part A

nowait

取消隐式的 barrier,也就是不让线程在循环或指令后等待其他线程完成,而是直接继续执行后续代码。

1
2
3
4
5
#pragma omp for
for (int i = 0; i < N; i++) {
// do something
}
// 隐式 barrier:所有线程必须等到这里,才继续执行下面的代码
1
2
3
4
5
#pragma omp for nowait
for (int i = 0; i < N; i++) {
// do something
}
// 没有隐式 barrier,线程可以立即继续执行下面的代码
  • 当循环后面的代码不依赖于当前循环的完成结果时,可以加 nowait 提高并行效率。
  • 如果后续代码依赖当前循环的结果,不能用 nowait,否则会出现数据竞争或错误。

if

条件性地决定是否在并行区域创建线程。也就是说,可以根据一个布尔条件来选择执行并行版本还是串行版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <omp.h>

int main() {
int N = 5;

// 如果 N > 10 则使用并行计算, 否则使用串行计算
#pragma omp parallel if(N > 10)
{
// doing something
}

return 0;
}

atomic

保证某个内存操作是原子的(不可分割的),防止多个线程同时修改同一个变量时产生 竞争条件 (race condition)

但只能管一行语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <omp.h>

int main() {
int sum = 0;

#pragma omp parallel for
for (int i = 0; i < 1000; i++) {
#pragma omp atomic
sum += 1; // 原子操作,避免竞态条件
}

printf("sum = %d\n", sum);
return 0;
}

critical

作用等同于atomic,用于一段代码,保证一段代码 同一时刻只允许一个线程进入

1
2
3
4
5
6
7
8
9
int sum = 0;

#pragma omp parallel for
for (int i = 0; i < 10; i++) {
#pragma omp critical
{
sum += i; // 每个线程都会来执行,但一次只能进一个
}
}
  • single VS critical :
    • single = “只做一次,随便哪个线程做就行”
    • critical = “大家都要做,但得一个一个排队做,不能一起做”

master

指定一段代码由主线程(thread 0)执行,其余线程跳过,不等待

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <omp.h>

int main() {
#pragma omp parallel
{
int tid = omp_get_thread_num();

#pragma omp master
{
printf("This is printed only by the master thread (tid = %d)\n", tid);
}

printf("Thread %d reached here.\n", tid);
}
return 0;
}

只有主线程会执行里面的printf, 其他线程都执行外面的printf

orderd

用来保证在并行循环中某些操作按循环迭代的顺序执行,即使循环是并行执行的。

  • 当你用 #pragma omp for 并行循环时,每个迭代可能被不同线程同时执行。
  • 如果某些操作必须严格按照迭代顺序执行(比如输出、累加到共享数组),就可以使用 ordered。
  • 必须在 for 循环中加上 ordered 子句,然后在循环体内用 #pragma omp ordered 标记需要按顺序执行的部分。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <omp.h>

int main() {
#pragma omp parallel for ordered
for (int i = 0; i < 8; i++) {
// 可以并行计算 i*i
int val = i * i;

// 但是下面的打印顺序必须按 i 从 0 到 7
// 输出顺序一定是 i=0, i=1, i=2, …,即使循环是并行执行的。
#pragma omp ordered
printf("i=%d, val=%d\n", i, val);
}
return 0;
}

子句

shared

指定变量在多个线程间共享。

1
2
3
4
5
int sum = 0;
#pragma omp parallel for shared(sum)
for(int i = 0; i < 10; i++) {
sum += i; // 注意:可能出现竞争条件,需要 atomic 或 critical
}

private / firstprivate / lastprivate

  • private(var):每个线程有独立的 var 副本,原来的共享变量不会被线程修改。
  • firstprivate(var):每个线程有独立副本,并初始化为原来的值。
  • lastprivate(var):循环结束后,最后一次迭代(代码理论上的最后一次)的线程的 var 值写回原来的共享变量。
1
2
3
4
5
6
7
8
9
10
11
12
13
int x = 10;

// private
#pragma omp parallel for private(x)
for(int i = 0; i < 4; i++) {
x = i; // 每个线程修改自己的 x,不影响其他线程
}

// firstprivate
#pragma omp parallel for firstprivate(x)
for(int i = 0; i < 4; i++) {
printf("%d\n", x); // 每个线程初始 x = 10
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// lastprivate
int main() {
int x = 0;

#pragma omp parallel for lastprivate(x)
for (int i = 0; i < 10; i++) {
x = i; // 每个线程有自己的 x
printf("Thread %d: x = %d\n", omp_get_thread_num(), x);
}

printf("After loop, x = %d\n", x); // x = 9
return 0;
}

threadprivate

用于声明每个线程都有自己独立的全局或静态变量副本,每个线程访问的是自己独立的副本,互不干扰。

  • 默认情况下,全局变量是所有线程共享的
  • 如果你希望每个线程有自己的独立版本,可以用 threadprivate。
  • 和局部变量不同,局部变量天然就是线程私有的;threadprivate 针对的是全局或 static 变量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <omp.h>
#include <stdio.h>

int counter = 0; // 全局变量
#pragma omp threadprivate(counter)

int main() {
#pragma omp parallel num_threads(4)
{
counter = omp_get_thread_num(); // 每个线程写自己的值
printf("Thread %d: counter = %d\n", omp_get_thread_num(), counter);
}
return 0;
}

Thread 1: counter = 1
Thread 2: counter = 2
Thread 0: counter = 0
Thread 3: counter = 3

default

  • 设置并行区域中变量的默认属性:
    • default(none):所有变量必须显式声明 shared 或 private,安全
    • default(shared):未声明的变量默认共享
    • default(private):未声明的变量默认私有
1
2
3
4
5
6
int x, y;
#pragma omp parallel default(none) shared(x) private(y)
{
y = omp_get_thread_num();
x = y;
}

reduction

  • 对共享变量进行归约操作(累加、乘积、最大值、最小值等),保证结果正确。
1
2
3
4
5
6
7
int sum = 0;
#pragma omp parallel for reduction(+:sum)
for(int i = 0; i < 100; i++) {
sum += i; // 自动在各线程归约
}

printf("sum = %d\n",sum);

copyin

用来给 线程私有的全局或静态变量(用 threadprivate 声明) 初始化值。它把 主线程(thread 0)的值复制到所有线程的私有副本

换句话说,threadprivate 声明的变量每个线程都有自己的副本,而 copyin 可以用来统一初始化这些副本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <omp.h>

int counter = 0;
#pragma omp threadprivate(counter)

int main() {
counter = 42; // 主线程初始化

#pragma omp parallel copyin(counter)
{
printf("Thread %d: counter = %d\n", omp_get_thread_num(), counter);
counter += omp_get_thread_num(); // 每个线程修改自己的副本
}

printf("Main thread counter = %d\n", counter); // 主线程的值不变
return 0;
}

Thread 0: counter = 42
Thread 1: counter = 42
Thread 2: counter = 42
Thread 3: counter = 42
Main thread counter = 42
  • counter 是 threadprivate,每个线程有独立副本
  • copyin(counter) 把主线程的值(42)复制给每个线程的副本
  • 每个线程修改自己的 counter 不会影响其他线程
  • 主线程的 counter 保持原值

copyprivate

  • copyprivate 是 OpenMP 的 single 子句的一部分。
  • 作用:在 parallel 区域中,single 区块由一个线程执行,但这个线程的变量值可以复制给其他线程
  • 通常配合 #pragma omp single 使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <omp.h>

int main() {
int x;

#pragma omp parallel
{
#pragma omp single copyprivate(x)
{
x = 42; // 只有一个线程执行
printf("Single thread sets x = %d\n", x);
}

// 其他线程在这里就能使用刚刚初始化的 x
printf("Thread %d sees x = %d\n", omp_get_thread_num(), x);
}

return 0;
}

// 1. #pragma omp single:parallel 区域中只有一个线程执行单独的代码块。
// 2. copyprivate(x):single 线程初始化的 x 值复制给所有线程的私有副本。
// 3. 其他线程即使没有执行 single 内的代码,也能得到相同的 x 值。

collapse

collapse(n) 用来 把 n 层嵌套循环合并为一个大的迭代空间,再分配给线程执行。

1
2
3
4
5
6
7
8
// 没有collapse(n)
// 只有外层循环 i 是被 OpenMP 分配给线程并行执行的,内层循环 j 在每个线程中是顺序执行的
#pragma omp parallel for
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
printf("Thread %d: i=%d, j=%d\n", omp_get_thread_num(), i, j);
}
}
1
2
3
4
5
6
7
8
// 有collapse
// 这时 OpenMP 会把外层和内层循环合并成 16 个迭代,然后分配给线程
#pragma omp parallel for collapse(2)
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
printf("Thread %d: i=%d, j=%d\n", omp_get_thread_num(), i, j);
}
}
  • collapse = 把多层循环“摊平”成一层迭代空间
  • 适合嵌套循环层次少,迭代次数不够多的情况
  • 可以与 schedule 结合,提高负载均衡

任务调度

schedule

  • schedule 是 OpenMP 的 for 或 parallel for 子句的一部分,用来控制 循环迭代分配给线程的策略
  • 主要作用:决定线程如何分配迭代任务,以及每次分配多少个迭代
  • type:static, dynamic, guided, runtime(真正意义上不算)
  • chunck_size:块大小
1
schedule(type[,chunk_size])
类型 说明 示例
static 循环迭代 均匀分块 分配给线程。默认 chunk_size = N / num_threads #pragma omp for schedule(static)
static, chunk_size 每个线程一次分配 chunk_size 个迭代,按顺序循环分配 #pragma omp for schedule(static, 2)
dynamic 循环迭代 动态分配,线程完成后再拿新的迭代块。适合负载不均的循环 #pragma omp for schedule(dynamic, 3)
guided 每次分配迭代块大小 逐渐减小,前期大块,后期小块。如果不指定chunk size,最小可以到1 #pragma omp for schedule(guided, 2)
auto 让编译器/运行时自动选择调度策略 #pragma omp for schedule(auto)
runtime 运行时根据环境变量 OMP_SCHEDULE 决定 #pragma omp for schedule(runtime)
  • static:负载均匀,开销小
  • dynamic:负载不均,线程可以“抢活”,开销略大
  • guided:负载递减型动态分配
  • auto / runtime:交给编译器或环境变量控制

task

  • Task 是 OpenMP 的一个并行单位,表示一段可以延迟执行的代码块。
  • 可以想象成一个 “工作单元”,由线程池中的线程去执行。
  • 与 parallel for 不同,它不一定是循环的迭代,也可以是任意代码块。
  • Task 可以有 依赖关系(depend),保证任务之间的执行顺序。

特点:

  1. 延迟执行:创建 task 时,OpenMP 不会立即执行它,而是放入任务队列等待线程执行。
  2. 灵活性高:可以表示任意代码,不只是循环。
  3. 线程复用:多个 task 可以被线程池中的空闲线程执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <omp.h>

int main() {
#pragma omp parallel
{
#pragma omp single // 保证只有一个线程创建任务
{
#pragma omp task
printf("Task 1 executed by thread %d\n", omp_get_thread_num());

#pragma omp task
printf("Task 2 executed by thread %d\n", omp_get_thread_num());
}
}
return 0;
}
  • single 保证任务只被创建一次。
  • 任务可以被任意空闲线程执行。
depend

在计算前缀和、Fibonacci 或矩阵块操作中,任务可能需要依赖其他任务完成的结果。

1
2
3
4
#pragma omp task depend(in: a) depend(out: b)
{
// task code
}
  • in: 表示依赖输入,任务必须等待这些数据准备好。
  • out: 表示任务会写这个数据。
  • inout: 表示任务既读又写。
1
2
3
4
5
6
7
8
9
10
// 二维前缀和
#pragma omp parallel
#pragma omp single
for (int x = 0; x < N; x++) {
for (int y = 0; y < N; y++) {
#pragma omp task depend(in: B[x-1][y], B[x][y-1], B[x-1][y-1]) depend(out: B[x][y])
B[x][y] = B[x-1][y] + B[x][y-1] - B[x-1][y-1] + A[x][y];
}
}
// B[x][y] 需要依赖 B[x-1][y], B[x][y-1], B[x-1][y-1] 的值
taskwait

用于 等待当前线程生成的所有子任务完成,然后再继续往下执行。

  • 它只等待 同一线程创建的任务
  • 不会等待其他线程创建的任务(除非它们是依赖任务)。
  • 放在并行区域或单线程区域中。
  • 后面不跟任何语句,只是一个同步点。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
#include <omp.h>

int main() {
#pragma omp parallel
#pragma omp single
{
#pragma omp task
printf("Task 1\n");

#pragma omp task
printf("Task 2\n");

// 等待上面所有任务完成
#pragma omp taskwait

printf("All tasks finished!\n");
}
return 0;
}

// 输出示意(顺序可能不同,但 All tasks finished! 一定在前两个任务后执行):
Task 1
Task 2
All tasks finished!

环境变量与 API 对照表

功能类别 环境变量 对应 API(代码里设置) 说明
线程数 OMP_NUM_THREADS omp_set_num_threads(int n) omp_get_num_threads() 控制并行区域的线程数
动态线程调整 OMP_DYNAMIC (TRUE/FALSE) omp_set_dynamic(int flag) omp_get_dynamic() 是否允许运行时调整线程数
线程总上限 OMP_THREAD_LIMIT omp_get_thread_limit() 限制整个程序最多线程数
嵌套并行 OMP_NESTED(旧标准) OMP_MAX_ACTIVE_LEVELS omp_set_max_active_levels(int levels) omp_get_max_active_levels() 设置并行嵌套层数
循环调度 OMP_SCHEDULE omp_set_schedule(omp_sched_t kind, int chunk) omp_get_schedule(&kind, &chunk) 控制 for 循环调度策略
绑定和亲和性 OMP_PROC_BIND omp_get_proc_bind() 控制线程是否绑定到 CPU
OMP_PLACES 无(环境变量指定) 线程可绑定的位置(cores/threads/sockets)
任务 OMP_MAX_TASK_PRIORITY 控制任务最大优先级
调试 & 性能 OMP_DISPLAY_ENV (TRUE/FALSE) 是否显示 OpenMP 环境设置
OMP_WAIT_POLICY (ACTIVE/PASSIVE) 控制空闲线程等待策略
OMP_CANCELLATION (TRUE/FALSE) omp_get_cancellation() 是否允许取消任务/循环
1
2
3
4
export OMP_NUM_THREADS=8
export OMP_SCHEDULE="dynamic,2"
export OMP_PROC_BIND=spread
export OMP_DISPLAY_ENV=TRUE

OpenMP 常用函数

1.线程管理

函数 说明
omp_set_num_threads(int n) 设置并行区域中的线程数(仅对后续并行区有效)
omp_get_num_threads() 获取当前并行区域的线程数
omp_get_thread_num() 获取当前线程编号(0 ~ n-1)
omp_get_max_threads() 返回可能使用的最大线程数
omp_set_dynamic(int flag) 开启/关闭动态调整线程数
omp_get_dynamic() 判断是否允许动态调整线程数
omp_get_thread_limit() 获取程序允许的最大线程数

2.并行控制

函数 说明
omp_in_parallel() 判断当前是否在并行区域内
omp_set_nested(int flag) (旧) 设置是否支持嵌套并行(OMP 4.0 前)
omp_set_max_active_levels(int levels) 设置最大并行嵌套层数
omp_get_max_active_levels() 获取最大嵌套层数
omp_get_level() 获取当前并行嵌套层数
omp_get_ancestor_thread_num(int level) 获取某层的祖先线程编号

3.调度与任务

函数 说明
omp_set_schedule(omp_sched_t kind, int chunk) 设置循环调度策略(static, dynamic, guided, auto)
omp_get_schedule(omp_sched_t *kind, int *chunk) 获取当前调度策略和 chunk 大小
omp_get_wtime() 获取墙钟时间(高精度计时)
omp_get_wtick() 获取 omp_get_wtime() 的精度
omp_get_num_procs() 获取可用的处理器数
omp_get_proc_bind() 获取线程绑定策略

4.任务与取消

函数 说明
omp_get_cancellation() 是否支持取消任务
omp_get_default_device() 获取默认设备(比如 GPU 设备 ID)
omp_set_default_device(int device_num) 设置默认设备

5.锁(同步机制)

函数 说明
omp_init_lock(omp_lock_t *lock) 初始化锁
omp_destroy_lock(omp_lock_t *lock) 销毁锁
omp_set_lock(omp_lock_t *lock) 获取锁(阻塞等待)
omp_unset_lock(omp_lock_t *lock) 释放锁
omp_test_lock(omp_lock_t *lock) 尝试获取锁(不阻塞,立即返回)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <omp.h>

int main() {
printf("最大线程数: %d\n", omp_get_max_threads());
printf("处理器数: %d\n", omp_get_num_procs());

double t1 = omp_get_wtime();

#pragma omp parallel
{
int id = omp_get_thread_num();
printf("线程 %d / %d 正在运行\n", id, omp_get_num_threads());
}

double t2 = omp_get_wtime();
printf("耗时: %f 秒\n", t2 - t1);

return 0;
}

OpenMP 编程常见坑点总结

1.循环迭代变量没声明为 private

写了平行区域 + for 循环,但是忘了加 private(i) → 会发生数据竞争。

1
2
3
4
5
6
#pragma omp parallel   // 创建多个线程
{
for (int i = 0; i < 10; i++) {
printf("Thread %d: i=%d\n", omp_get_thread_num(), i);
}
}
  • 这里 i 是 共享的(shared),所有线程同时用 同一个变量 i
  • 当多个线程同时修改 i,就会发生 数据竞争 (race condition)
  • English: The loop variable i is shared across threads, so multiple threads update it at the same time, causing a race condition.

解决办法,加上private(i) 或者使用parallel for(会自动把 i 设置成private)

1
2
3
4
5
6
7
8
9
10
11
#pragma omp parallel private(i)
{
for (int i = 0; i < 10; i++) {
printf("Thread %d: i=%d\n", omp_get_thread_num(), i);
}
}

#pragma omp parallel for
for (int i = 0; i < 10; i++) {
printf("Thread %d: i=%d\n", omp_get_thread_num(), i);
}

2.数据竞争(race condition)

  • 多个线程同时写共享变量 → 结果不确定。
  • 常见于计数、累加。
  • English: Shared variables modified by multiple threads without synchronization will lead to unpredictable results.
1
2
3
4
5
6
7
8
int sum = 0;
#pragma omp parallel for
for (int i = 0; i < 1000; i++) {
sum++; // ❌ 多线程同时写 sum → 结果随机
}
// ✅ 用 atomic 或 reduction
#pragma omp parallel for reduction(+:sum)
for (int i = 0; i < 1000; i++) sum++;

3.reduction

忘记 reduce
  • 很多人写并行求和、最值时直接 shared → 结果错误。
  • English: Without reduction, all threads write to the same shared variable at the same time, causing wrong results.
1
2
3
4
5
6
7
int sum = 0;
#pragma omp parallel for
for (int i = 0; i < N; i++) sum += A[i]; // ❌ 错

// ✅ 需要加 reduction()
#pragma omp parallel for reduction(+:sum)
for (int i = 0; i < N; i++) sum += A[i];
reduction 忘记初始化
  • reduction 会在每个线程里生成 sum 的副本,但如果 sum 没初始化,结果是随机的。
  • English: If the reduction variable is not initialized, each thread’s private copy starts with an undefined value, leading to incorrect results.
1
2
3
int sum;
#pragma omp parallel for reduction(+:sum)
for (int i=0; i<10; i++) sum += i; // ❌ sum 未初始化
1
2
3
4
// ✅正确写法
int sum = 0;
#pragma omp parallel for reduction(+:sum)
for (int i=0; i<10; i++) sum += i;

4.barrier / nowait 使用不当

  • 默认 for 和 sections 结束时会有隐式 barrier。
  • 如果用 nowait,要小心后续代码是否需要同步。
  • English: Using nowait skips synchronization, so some threads may read data before others finish writing.
1
2
3
4
5
6
7
8
#pragma omp parallel
{
#pragma omp for nowait
for (int i=0;i<10;i++) A[i]=i;

// ❌ 这里直接用 A[i],可能有线程还没写完
printf("Thread %d: A[5] = %d\n", omp_get_thread_num(), A[5]);
}
  • 所以有些线程可能还在写 A[i],但别的线程已经跑到 printf 了。
  • 结果就可能访问到 未初始化或部分更新的 A,产生 竞态条件 (race condition)

去掉 nowait(默认会有 barrier,同步所有线程):

1
2
3
4
5
6
7
8
#pragma omp parallel
{
#pragma omp for // 默认带 barrier
for (int i=0;i<10;i++) A[i]=i;

// ✔️ 到这里时,所有 A[i] 都保证写完
printf("Thread %d: A[5] = %d\n", omp_get_thread_num(), A[5]);
}

保留 nowait,但手动加 barrier

1
2
3
4
5
6
7
8
9
#pragma omp parallel
{
#pragma omp for nowait
for (int i=0;i<10;i++) A[i]=i;

#pragma omp barrier // ✔️ 等所有线程完成写操作

printf("Thread %d: A[5] = %d\n", omp_get_thread_num(), A[5]);
}

5.task 忘记加 taskwait

  • 如果 task 有依赖关系,必须手动同步。
  • English: Without taskwait, the program may continue before all tasks have finished, leading to incomplete results.
1
2
3
4
5
6
7
8
9
10
11
12
#pragma omp parallel
{
#pragma omp single
{
#pragma omp task
do_A();
#pragma omp task
do_B();
// ❌ 如果这里直接结束,B 可能还没完成
#pragma omp taskwait // ✅ 等待所有 task 完成
}
}

6.循环嵌套没有 collapse

  • 多重循环并行时,只 parallel 外层 → 内层仍是串行。
  • English: Without collapse, only the outer loop is parallelized, so the workload may not be fully distributed.
1
2
3
4
5
6
7
8
9
#pragma omp parallel for
for (int i=0;i<N;i++)
for (int j=0;j<N;j++)
work(i,j); // ❌ 内层没并行
// ✅
#pragma omp parallel for collapse(2)
for (int i=0;i<N;i++)
for (int j=0;j<N;j++)
work(i,j);

7. schedule 不当

  • schedule(static) 适合工作量均匀的循环;
  • schedule(dynamic, chunk) 适合负载不均的循环;
  • 乱用可能导致线程负载不平衡。
  • English: Using the wrong scheduling strategy can cause load imbalance, where some threads finish early and others are overloaded.

8.乱用 critical / atomic

  • critical 会严重拖慢性能,如果只是单行更新 → 用 atomic。

  • English: critical forces threads to serialize access, which is slower; if only a single statement is updated, atomic is better.

1
2
3
4
5
6
7
8
#pragma omp parallel for
for (int i=0;i<N;i++) {
#pragma omp critical // ❌ 太重
sum += A[i];
}
// ✅ 更快:
#pragma omp atomic
sum += A[i];

9.输出打印错乱

  • printf 是共享 I/O,多个线程同时写会交错。
  • English: Standard output is shared; multiple threads writing at the same time will interleave their messages.
  • 如果要有序输出,用 ordered 或加锁。
1
2
3
4
5
#pragma omp parallel for ordered
for (int i=0;i<8;i++) {
#pragma omp ordered
printf("i=%d\n", i);
}

10.忘记设置默认共享/私有变量(default 子句)

  • x 是共享变量(shared),但被多个线程同时修改

  • x is a shared variable, but it is being modified simultaneously by multiple threads.

1
2
3
4
5
6
int x = 10, y = 20;
#pragma omp parallel
{
x += omp_get_thread_num(); // ❌ 数据竞争
printf("Thread %d: x=%d\n", omp_get_thread_num(), x);
}
  • ✅解决办法,明确写出
1
#pragma omp parallel default(none) private(x) shared(y)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 每个线程有自己的 x 副本:
int x = 10, y = 20;
#pragma omp parallel private(x)
{
x = 10; // 每个线程初始化自己的副本
x += omp_get_thread_num();
printf("Thread %d: x=%d\n", omp_get_thread_num(), x);
}

// x 是共享的,但所有线程的更新结果能累加:
int x = 10;
#pragma omp parallel reduction(+:x)
{
x += omp_get_thread_num();
}
printf("Final x = %d\n", x);

11. PPT

  1. #pragma omp parallel for num_threads(P)
      for (int i = 0; i < N; i++) {
    #pragma omp task
        h(i);
      }
    
    1
    2
    3
    4
    5
    6
    7

    - 在 OpenMP 中,如果没有明确的同步机制(如 `taskwait` 或 `barrier`),父任务(在这里是循环迭代)可以在子任务(h(i))完成前就结束。
    - 当所有循环迭代完成后,parallel 区域会立即结束。此时,可能有许多任务(h(i))尚未执行完成。
    - Explicit synchronization mechanisms (such as taskwait or barrier) cause the parallel region to end immediately after all loop iterations are completed. At this point, many tasks (h(i)) may not have finished executing.

    **在 single 区域中创建任务并等待**:

    #pragma omp parallel num_threads(P) { #pragma omp single { for (int i = 0; i < N; i++) { #pragma omp task h(i); } #pragma omp taskwait // 等待所有任务完成 } }