OpenMP
介绍
- 代码片段
#include <stdlib.h>
#include <stdio.h>
#include <omp.h> // OpenMP编程需要包含的头文件
int main()
{
#pragma omp parallel for
for(int i = 0; i < 10; ++i)
{
fprintf(stderr, "%d\n", i);
}
return 0;
};
- 编译
gcc -o a.out main.c -fopenmp
- 片段解释
code | comments |
---|---|
#pragma omp single | 开始一个单线程执行域。 |
#pragma omp parallel | 开始一个多线程执行域。 |
#pragma omp for | 在并行域中,将 for() {} 语句撤分为多个线程执行。 |
#pragma omp | 在并行域中,使用单独的线程执行紧接的 for() {} 等。 |
常用函数
- 原型说明
function | comments |
---|---|
int omp_get_num_procs(void) | 返回当前可用的处理器个数。 |
int omp_get_num_threads(void) | 返回当前并行区域中的活动线程个数,如果在并行区域外部调用,返回1。 |
int omp_get_thread_num(void) | 返回当前的线程号。 |
int omp_set_num_threads(void) | 设置进入并行区域时,将要创建的线程个数。 |
- 并行区域
#include <stdlib.h>
#include <stdio.h>
#include <omp.h> // OpenMP编程需要包含的头文件
int main()
{
fprintf(stderr, "procs : %d\n", omp_get_num_procs());
#pragma omp parallel //指明下面大括号内部为并行区域
{
fprintf(stderr, "%d\n", i);
}
return 0;
}
- 示例代码
#include <iostream>
#include <omp.h> // OpenMP编程需要包含的头文件
int main()
{
std::cout << "CPU number: " << omp_get_num_procs() << std::endl;
std::cout << "Parallel area 1: " << std::endl;
#pragma omp parallel //指明下面大括号内部为并行区域
{
std::cout << "Num of threads is " << omp_get_num_threads();
std::cout << "; This thread ID is " << omp_get_thread_num() << std::endl;
}
std::cout << "Parallel area 2: " << std::endl;
omp_set_num_threads(4); // 设置成为并行区域创建4个线程
#pragma omp parallel //指明下面大括号内部为并行区域
{
std::cout << "Num of threads is " << omp_get_num_threads();
std::cout << "; This thread ID is " << omp_get_thread_num() << std::endl;
}
return 0;
}
数据的共享与私有化
- 定义
除了并行区域中定义的变量、循环条件变量、private、firstprivate、lastprivate或reduction字句修饰的变量,并行区域中的所有变量都是共享的。例如:
#include <iostream>
#include <omp.h> // OpenMP编程需要包含的头文件
int main()
{
int share_a = 0; // 共享变量
int share_to_private_b = 1; //通过private子句修饰该变量之后在并行区域内变为私有变量
#pragma omp parallel
{
int private_c = 2;
#pragma omp for private(share_to_private_b)
for (int i = 0; i < 10; ++i) //该循环变量是私有的,若为两个线程,则一个线程执行0 <= i < 5,另一个线程执行5 <= i < 10
{
std::cout << i << std::endl;
}
}
return 0;
}
- 共享与私有变量声明的方法
keyword | comments |
---|---|
private(val1, val2, ...) | 并行区域中变量val是私有的,即每个线程拥有该变量的一个拷贝,但都各自被初始化为0。 |
firstprivate(val1, val2, ...) | 与 private 不同的是,每个线程在开始循环时将会拷贝变量的初始化值。 |
lastprivate(val1, val2, ...) | 与 private 不同的是,每个线程在退出循环时将私有变量将会拷回到原变量中,一般配合 firstprivate 使用。 |
shared(val1, val2, ...) | 声明val是共享的。 |
- 示例代码
#include <iostream>
#include <omp.h> // OpenMP编程需要包含的头文件
int main()
{
int shared_to_private = 1;
#pragma omp parallel for private(shared_to_private)
for (int i = 0; i < 10; ++i)
{
std::cout << shared_to_private << std::endl;
}
return 0;
}
有效数据规约
- 关键字
reduction
说明对变量的操作原则,多个线程的执行结果通过reduction中声明的操作符进行计算。以加法为例 reduction (+: sum)
,假设sum的初始值为10,reduction(+: sum)
声明的并行区域中每个线程的sum初始值为0(规定),并行处理结束之后,会将sum的初始化值10以及每个线程所计算的sum值相加。
- 声明
reduction (operator: var1, val2, ...)
operator | data type | initial |
---|---|---|
+ | 整数、浮点 | 0 |
- | 整数、浮点 | 0 |
* | 整数、浮点 | 1 |
& | 整数 | 所有位均为 1 |
| | 整数 | 0 |
^ | 整数 | 0 |
&& | 整数 | 1 |
|| | 整数 | 0 |
- 示例
#include <iostream>
#include <omp.h> // OpenMP编程需要包含的头文件
int main()
{
int sum = 0;
std::cout << "Before: " << sum << std::endl;
#pragma omp parallel for reduction(+: sum)
for (int i = 0; i < 10; ++i)
{
sum = sum + i;
std::cout << sum << std::endl;
}
std::cout << "After: " << sum << std::endl;
return 0;
}
atomic操作
- 声明
#pragma opm atomic
适用于对变量的复合运算和自增减运算。比如:
#pragma opm atomic
x |= (1 << 2)
- 示例代码
#include <iostream>
#include <omp.h> // OpenMP编程需要包含的头文件
int main()
{
int sum = 0;
std::cout << "Before: " << sum << std::endl;
#pragma omp parallel for
for (int i = 0; i < 20000; ++i)
{
#pragma omp atomic
sum++;
}
std::cout << "After: " << sum << std::endl;
return 0;
}
临界区同步
- 简介
critical与atomic的区别在于,atomic仅适用于上一节规定的两种类型操作,而且atomic所防护的仅为一句代码。critical可以对某个并行程序块进行防护。
- 声明
#pragma omp critical [(named)] //[]表示命名是可选
{
//并行程序块,同时只能有一个线程能访问该并行程序块
}
比如:
#prgama omp critical (a)
a = b + c;
- 示例
#include <iostream>
#include <omp.h> // OpenMP编程需要包含的头文件
int main()
{
int sum = 0;
std::cout << "Before: " << sum << std::endl;
#pragma omp parallel for
for (int i = 0; i < 100; ++i)
{
#pragma omp critical (a)
{
sum = sum + i;
sum = sum + i * 2;
}
}
std::cout << "After: " << sum << std::endl;
return 0;
}
互斥锁函数
- 定义
类似 Mutex
- 原型说明
funciton | comments |
---|---|
void opm_init_lock(omp_lock *) | 初始化互斥器 |
void opm_destroy_lock(omp_lock *) | 销毁互斥器 |
void opm_set_lock(omp_lock *) | 获得互斥器 |
void opm_unset_lock(omp_lock *) | 释放互斥器 |
void opm_test_lock(omp_lock *) | 试图获得互斥器,如果获得成功返回true,否则返回false |
- 示例代码
#include <iostream>
#include <omp.h> // OpenMP编程需要包含的头文件
static omp_lock_t lock;
int main()
{
omp_init_lock(&lock); // 初始化互斥锁
#pragma omp parallel for
for (int i = 0; i < 5; ++i)
{
omp_set_lock(&lock); //获得互斥器
std::cout << omp_get_thread_num() << "+" << std::endl;
std::cout << omp_get_thread_num() << "-" << std::endl;
omp_unset_lock(&lock); //释放互斥器
}
omp_destroy_lock(&lock); //销毁互斥器
return 0;
}
事件同步机制
- 隐式栅障
前面所有的示例都带有隐式栅障,即并行区域中所有线程执行完毕之后,主线程才继续执行。
- 同步关键字
keyword | function | example |
---|---|---|
nowait | 不等待指定的并行语句执行完成,继续执行下面的语句。 | #pragma omp for nowait |
barrier | 显示同步,等待前面的线程结束。 | #pragma omp barrier |
master | 声明对应的并行程序块只由主线程完成。 | #pragma omp mater |
section | 用来指定不同的线程执行不同的部分。 | #pragma omp parallel sections {} #pragma omp section {} |
- 示例代码
#include <iostream>
#include <omp.h> // OpenMP编程需要包含的头文件
int main()
{
#pragma omp parallel
{
#pragma omp for nowait
for (int i = 0; i < 1000; ++i)
{
std::cout << i << "+" << std::endl;
}
#pragma omp for
for (int j = 0; j < 10; ++j)
{
std::cout << j << "-" << std::endl;
}
}
return 0;
}
#include <iostream>
#include <omp.h> // OpenMP编程需要包含的头文件
int main()
{
#pragma omp parallel
{
for (int i = 0; i < 100; ++i)
{
std::cout << i << "+" << std::endl;
}
#pragma omp barrier
for (int j = 0; j < 10; ++j)
{
std::cout << j << "-" << std::endl;
}
}
return 0;
}
#include <iostream>
#include <omp.h> // OpenMP编程需要包含的头文件
int main()
{
#pragma omp parallel
{
#pragma omp master
{
for (int j = 0; j < 10; ++j)
{
std::cout << j << "-" << std::endl;
}
}
std::cout << "This will printed twice." << std::endl;
}
return 0;
}
#include <iostream>
#include <omp.h> // OpenMP编程需要包含的头文件
int main()
{
#pragma omp parallel sections //声明该并行区域分为若干个section,section之间的运行顺序为并行的关系
{
#pragma omp section //第一个section,由某个线程单独完成
for (int i = 0; i < 5; ++i)
{
std::cout << i << "+" << std::endl;
}
#pragma omp section //第一个section,由某个线程单独完成
for (int j = 0; j < 5; ++j)
{
std::cout << j << "-" << std::endl;
}
}
return 0;
}
调度策略
- 说明
默认情况下会自动生成与CPU个数相等的线程,然后并行执行并行区域中的代码。通过对并行区域中的for循环进行特殊的声明,可以将for循环的不同部分分配给不同的线程运行。然后再通过锁同步或事件同步来实现并行区域的同步控制。
- 调度策略
policy | comments | condition |
---|---|---|
static | 循环变量区域分为n等份,每个线程评分n份任务 | 各个cpu的性能差别不大 |
dynamic | 循环变量区域分为n等份,某个线程执行完1份之后执行其他需要执行的那一份任务 | cpu之间运行能力差异较大 |
guided | 循环变量区域由大到小分为不等的n份,运行方法类似于 dynamic | 由于任务份数比 dynamic ,所以可以减少调度开销 |
runtime | 在运行时来适用上述三种调度策略中的一种,默认是使用 static |
- 声明
使用 ... for schedule(<policy>, n)
将并行任务按 policy
调度策略,for
循环每 n
迭代分成一个任务。
- 示例
#include <iostream>
#include <omp.h> // OpenMP编程需要包含的头文件
int main()
{
#pragma omp parallel for schedule(static, 2) //static调度策略,for循环每两次迭代分成一个任务
for (int i = 0; i < 10; ++i) //被分成了5个任务,其中循环0~1,4~5,8~9分配给了第一个线程,其余的分配给了第二个线程
{
std::cout << "Thread ID:" << omp_get_thread_num() << " Value:" << i << std::endl;
}
return 0;
}
#include <iostream>
#include <omp.h> // OpenMP编程需要包含的头文件
int main()
{
#pragma omp parallel for schedule(dynamic, 2) //dynamic调度策略,for循环每两次迭代分成一个任务
for (int i = 0; i < 10; ++i) //被分成了5个任务,只要有任务并且线程空闲,那么该线程会执行该任务
{
std::cout << "Thread ID:" << omp_get_thread_num() << " Value:" << i << std::endl;
}
return 0;
}