0%

openmp入门

开一篇博客,并试图逼自己入门简单的openmp并行计算

头文件:#include <omp.h>

omp parallel

子句将被多线程执行。如不指定线程数,一般默认CPU线程数

1
// parallel: 将被多个线程执行
2
#pragma omp parallel num_threads(4)
3
{
4
    printf("Hello World! ThreadID = %d\n", omp_get_thread_num());
5
}
6
/*
7
Hello World! ThreadID = 3
8
Hello World! ThreadID = 0
9
Hello World! ThreadID = 1
10
Hello World! ThreadID = 2
11
*/

omp for

omp for只在自身处于omp parallel环境下才有意义。

omp for子句中首行需要是可以预测的for循环语句(即某个变量的区间线性变换)。

该for循环会被不同线程刮分为尽可能等长的部分,分配方式由编译器决定。

1
// for: 多线程并行for循环
2
int sum = 0;
3
#pragma omp parallel num_threads(8) shared(sum)
4
{
5
    int mysum = 0;
6
    #pragma omp for
7
    for (int i = 0; i <= 10000; ++i){
8
        mysum += i;
9
    }
10
    printf("ThreadID = %d, partial sum = %d\n", omp_get_thread_num(), mysum);
11
    #pragma omp critical
12
    {
13
        sum += mysum;
14
    }
15
}
16
printf("%d\n", sum);
17
18
/*
19
ThreadID = 3, partial sum = 5469375
20
ThreadID = 1, partial sum = 2344375
21
ThreadID = 2, partial sum = 3906875
22
ThreadID = 4, partial sum = 7031875
23
ThreadID = 5, partial sum = 8594375
24
ThreadID = 0, partial sum = 781875
25
ThreadID = 6, partial sum = 10156875
26
ThreadID = 7, partial sum = 11719375
27
50005000
28
*/

注意:

  • 子句外面的sum默认是shared的,子句里面的mysum默认是private的
  • 如果需要将外部变量改为线程私有可以用private(...)firstprivate(...)
  • 通过部分和可以看到,实验环境中的线程分配逻辑为0号拿0-1250,1号拿1251-2500等
  • 在对共享变量做写操作时,需要加锁保证线程安全
    • 例如此处的sum += mysum;
    • 如果需要出错的实验现象,可以将线程数改成2000。

omp parallel for

将parallel和for写在一起,上述代码的等价代码如下:

1
// parallel for: 等价代码,注意这里的sum无需写private
2
sum = 0;
3
#pragma omp parallel for num_threads(8) reduction(+: sum)
4
for (int i = 0; i <= 10000; ++i){
5
    sum += i;
6
}
7
printf("%d\n", sum);
8
/*
9
50005000
10
*/

这里的sum实际上还是被多个线程私有化了,加写private(sum)会报错。

如果在并行区域内不加锁保护就直接对共享变量迚行写操作,存在数据竞争问题,会导致不可预测的异常结果。

共享数据作为 private、firstprivate、lastprivate、threadprivate、reduction 子句的参数进入并行区域后,就变成线程私有了,不需要加锁保护了。

reductin(运算符: 变量)会将每个线程最终得到的变量结果以运算符方式归入变量中去。

注意reduction的用法:

运算标识符 各线程私有变量初始值
+ 0
* 1
- 0
& ~0

omp parallel sections

将每个section手动分配给一个线程(如果线程数不够还是会调度做)进行并行

1
sum = 0;
2
// parallel sections: 多个线程执行若干section
3
#pragma omp parallel sections firstprivate(sum) lastprivate(sum)
4
{
5
    #pragma omp section
6
    {
7
        for (int i = 0; i <= 10000; ++i){
8
            sum += i;
9
        }
10
        printf("ThreadID = %d, sum = %d\n", omp_get_thread_num(), sum);
11
    }
12
    #pragma omp section
13
    {
14
        for (int i = 0; i <= 100; ++i){
15
            sum -= i;
16
        }
17
        printf("ThreadID = %d, sum = %d\n", omp_get_thread_num(), sum);
18
    }
19
}
20
printf("%d\n", sum);
21
/*
22
ThreadID = 3, sum = -5050
23
ThreadID = 2, sum = 50005000
24
-5050
25
*/

为了保证并行的效率性:

  • 在使用sections时需要尽可能保障每个section的执行时间尽可能相同
  • 就像使用parallel for时需要尽可能保障每个线程执行时间尽可能相同
  • 否则就可能发生一核有难五核忙等的悲剧

常用参数

  • num_threads(x):指定线程数
  • private(x,y,...):指定变量为线程私有,初始值未知;原变量的值不会改变
  • firstprivate(x,y,...):指定变量为私有,且需要拷贝主线程的初始值;原变量值不会被改变
  • lastprivate(x,y,...):指定变量为私有,初始值未知,在退出并行后,会将serial理论上的最后一次结果复制给同名原变量
  • shared(x,y,...):指定变量为线程共享,注意在写的时候需要加锁
  • critical:申明一段临界区
  • reduction(op: x):指定每个线程创建x的一个私有拷贝, 在区域的结束处,将用私有拷贝的值通过指定的op运行符运算,并对原始的参数条目被运算结果的值更新。
  • omp_get_thread_num():得到当前线程号

更多内容可以参考此处