OpenMP通过在串行程序中插入编译制导指令, 来实现并行化, 支持OpenMP的编译器可以识别, 处理这些指令并实现对应的功能. 所有的编译制导指令都是以#pragma omp开始, 后面跟具体的功能指令(directive)或者命令. 一般格式如下所示:
#pragma omp directive [clause [[,] clause]...]
structured bloc
为了使程序可以并行执行, 我们首先要构造一个并行域(parallel region), 在这里我们使用parallel指令来实现并行域的构造, 其语法形式如下
#pragma omp parallel [clause [[,] clause]...]
structured block
parallel :用在一个结构块之前,表示这段代码将被多个线程并行执行;该指令只保证代码以并行的方式执行, 但是并不负责线程之间的任务分发. 在并行域执行结束之后, 会有一个隐式的屏障(barrier), 来同步所有的该区域内的所有线程. 下面是一个使用示例:
#include
#include
using namespace std;
void fun1()
{
#pragma omp parallel num_threads(5) //定义5个线程,每个线程都将运行{}内代码,运行结果:输出6次Test
{
cout << "Test" << endl;
}
}
int main()
{
fun1();
}
/*TestTestTestTest
Test*/
parallel指令后面可以跟一些子句(clause), 如下所示
if(scalar-expression)
num_threads(integer-expression)
private(list)
firstprivate(list)
shared(list)
default(none | shared)
copyin(list)
reduction(operator:list)
后面介绍这些从句用法
任务分担指令主要用于为线程分配不同的任务, 一个任务分担域(work-sharing region)必须要和一个活跃(active)的并行域(parellel region)关联, 如果任务分担指令处于一个不活跃的并行域或者处于一个串行域中, 那么该指令就会被忽略. 在C/C++有3个任务分担指令: for、sections、single, 严格意义上讲只有for和sections是任务分担指令, 而single只是协助任务分担的指令.
Syntax in C++ | 功能 |
---|---|
#pragma omp for | Distribute iterations over the threads |
#pragma omp sections | Distribute independent work units |
#pragma omp single | Only one thread executes the code block |
用于for循环语句之前,表示将循环计算任务分配到多个线程中并行执行,以实现任务分担,必须由编程人员自己保证每次循环之间无数据相关性;
for指令一般可以和parallel指令合起来形成parallel for指令使用,也可以单独用在parallel语句的并行块中。parallel for用于生成一个并行域,并将计算任务在多个线程之间分配,用于分担任务。程序示例如下:
void parallel_for() {
int n = 9;
int i = 0;
#pragma omp parallel shared(n) private(i)
{
#pragma omp for
for(i = 0; i < n; i++) {
printf("Thread %d executes loop iteration %d\n", omp_get_thread_num(),i);
}
}
}
/*Thread 2 executes loop iteration 5
Thread 2 executes loop iteration 6
Thread 3 executes loop iteration 7
Thread 3 executes loop iteration 8
Thread 0 executes loop iteration 0
Thread 0 executes loop iteration 1
Thread 0 executes loop iteration 2
Thread 1 executes loop iteration 3
Thread 1 executes loop iteration 4
*/
在上面的程序中共有4个线程执行9次循环, 线程0分到了3次, 剩余的线程分到了2次, 这是一种常用的调度方式, 即假设有n次循环迭代, t个线程, 那么每个线程分配到n/t 或者 n/t + 1 次连续的迭代计算, 但是某些情况下使用这种方式并不是最好的选择, 我们可以使用schedule 来指定调度方式, 在后面会具体介绍. 下面是for 指令后面可以跟的一些子句:
private(list)
fistprivate(list)
lastprivate(list)
reduction(operator:list)
ordered
schedule(kind[,chunk_size])
nowait
下面还有一些示例
#include
#include
using namespace std;
int main()
{
#pragma omp parallel
for (int i = 0; i < 10; i++)
{
cout << i;
}
return 0;
}
/*00102132435671203010102430182132456756479849342516278956788345956934576677889899*/
咋一看程序好像抽风了,产生了4个线程同时执行了for循环。通常这不是我们想要的,我们想要的是把for中的任务等分成4份,分别由4个线程各执行其中的一份。这样做其实很简单,只要在parallel后面加上for就可以了。
#include
#include
using namespace std;
int main()
{
#pragma omp parallel for
for (int i = 0; i < 10; i++)
{
cout << i;
}
return 0;
}
//result: 2890156734
结果为对的, 因为在同一段并行代码中,我们并不能保证各线程执行的先后顺序。扩散开来,在并行代码中,各线程的工作不能有依赖性,比如如果一个线程的输入和另一个线程的输出相依赖,那么此程序不适合并行计算。
下面再看下面的例子
#include
#include
using namespace std;
int main()
{
//for循环并行化声明形式1
#pragma omp parallel
{
cout << "OK" << endl;
#pragma omp for
for (int i = 0; i < 4; ++i)
{
cout << i << endl;
}
}
cout << "形式2" << endl;
//for循环并行化声明形式2
#pragma omp parallel for
//cout << "ERROR" << endl;
for (int j = 0; j < 4; ++j)
{
cout << j << endl;
}
return 0;
}
这个结果为
形式1与形式2相比, 其优点是在for循环体前可以有其他执行代码,当然在一个#pragma omp parallel块内,可以有多个#pragma omp parallel for循环体。
尽管OpenMP可以方便地对for循环进行并行化,但并不是所有的for循环都可以进行并行化。以下几种情况不能进行并行化:
for循环中的循环变量必须是有符号整形。例如,for (unsigned int i = 0; i < 10; ++i){}会编译不通过;
for循环中比较操作符必须是<, <=, >, >=。例如for (int i = 0; i != 10; ++i){}会编译不通过;
for循环中的第三个表达式,必须是整数的加减,并且加减的值必须是一个循环不变量。例如for (int i = 0; i != 10; i = i + 1){}会编译不通过;感觉只能++i; i++; --i; 或i–;
如果for循环中的比较操作为<或<=,那么循环变量只能增加;反之亦然。例如for (int i = 0; i != 10; --i)会编译不通过;
循环必须是单入口、单出口,也就是说循环内部不允许能够达到循环以外的跳转语句,exit除外。异常的处理也必须在循环体内处理。例如:若循环体内的break或goto会跳转到循环体外,那么会编译不通过。
如果说for directive用作数据并行,那么sections directive用于任务并行,它指示后面的代码块包含将被多个线程并行执行的section块。下面是一个例子:sections指令可以为不同的线程分配不同的任务, 语法如下所示:
#pragma omp sections [clause[[,] clause]...]
{
[#pragma omp section]
structured block
[#pragma omp section]
structured block
...
}
从上面的代码中我们可以看到, sections将代码分为多个section, 每个线程处理一个section, 注意区分sections和section)下面是一个使用示例:
/**
* 使用#pragma omp sections 和 #pragma omp section, 来使不同的线程执行不同的任务
* 如果线程数量大于section数量, 那么多余的线程会处于空闲状态(idle)
* 如果线程数量少于section数量, 那么一个线程会执行多个section代码
*/
void funcA() {
printf("In funcA: this section is executed by thread %d\n",
omp_get_thread_num());
}
void funcB() {
printf("In funcB: this section is executed by thread %d\n",
omp_get_thread_num());
}
void parallel_section() {
#pragma omp parallel
{
#pragma omp sections
{
#pragma omp section
{
(void)funcA();
}
#pragma omp section
{
(void)funcB();
}
}
}
}
/*In funcA: this section is executed by thread 0
In funcB: this section is executed by thread 1
*/
还有一些例子
void test_sections() {
#pragma omp parallel
{
#pragma omp sections
{
#pragma omp section
cout << omp_get_thread_num()<<endl;
#pragma omp section
cout << omp_get_thread_num()<<endl;
}
}
}
int main() {
test_sections();
}
/* 1
4*/
上面代码中2个section块将被2个线程并行执行,2个section是被哪2个线程执行是不确定的,当section块多于8个时,会有一个线程执行不止1个section块
上面的代码也可以修改为
void test_sections() {
#pragma omp parallel sections
{
#pragma omp section
cout << omp_get_thread_num();
#pragma omp section
cout << omp_get_thread_num();
}
}
int main() {
test_sections();
}
下面是sections后面可以跟的一些子句
private(list)
firstprivate(list)
lastprivate(list)
reduction(operator:list)
nowait
single 指令用来指定某段代码块只能被一个线程来执行, 具体是哪个线程不确定. 如果没有nowait字句, 所有线程在 single 指令结束处隐式同步点同步, 如果single指令有nowait从句, 则别的线程直接往下执行. 语法如下所示:
#pragma omp single [clause[[,] clause]...]
structured block
下面是一个示例
void test_single() {
#pragma omp parallel num_threads(4)
{
#pragma omp single
cout << omp_get_thread_num() << endl;
cout << omp_get_thread_num() << " -";
}
}
/*
0
3 -1 -20 - -
这里0号线程执行了那两行代码,其他线程只执行后面那行代码
*/
void test_single() {
#pragma omp parallel num_threads(4)
{
#pragma omp single
{
cout << omp_get_thread_num() << endl;
cout << omp_get_thread_num() << " -";
}
}
}
/*
0
0 -
*/
另外一个示例
#include
#include
void parallel_single() {
int a = 0;
int n = 10;
int i;
int b[10];
#pragma omp parallel shared(a, b) private(i)
{
// 只有一个线程会执行这段代码, 其他线程会等待该线程执行完毕
#pragma omp single
{
a = 10;
printf("Single construct executed by thread %d\n", omp_get_thread_num());
}
// A barrier is automatically inserted here
#pragma omp for
for (i = 0; i < n; i++) {
b[i] = a;
}
}
printf("After the parallel region:\n");
for (i = 0; i < n; i++)
printf("b[%d] = %d\n", i, b[i]);
}
int main() {
parallel_single();
}
/*Single construct executed by thread 0
After the parallel region:
b[0] = 10
b[1] = 10
b[2] = 10
b[3] = 10
b[4] = 10
b[5] = 10
b[6] = 10
b[7] = 10
b[8] = 10
b[9] = 10
*/
下面是single指令后面可以跟随的子句:
private(list)
firstprivate(list)
copyprivate(list)
nowait
将parallel指令和work-sharing指令结合起来, 使代码更加简洁. 如下面的代码
#pragma omp parallel
{
#pragma omp for
for(.....)
}
也可以写为
#pragma omp parallel
{
#pragma omp for
for(.....)
}
具体参见下图
使用这些组合结构体(combined constructs)不仅增加程序的可读性, 而且对程序的性能有一定的帮助. 当使用这些组合结构体的时候, 编译器可以知道下一步要做什么, 从而可能会生成更高效的代码.
OpenMP指令后面可以跟一些子句, 用来控制构造器的行为. 下面介绍一些常用的子句.
shared子句可以用于声明一个或多个变量为共享变量。所谓的共享变量,是值在一个并行区域的team内的所有线程只拥有变量的一个内存地址,所有线程访问同一地址。所以,对于并行区域内的共享变量,需要考虑数据竞争条件,要防止竞争,需要增加对应的保护, 下面是其使用方法:
#pragma omp parallel for shared(a)
for(i = 0; i < n; i++)
{
a[i] += i;
}
在并行域中使用共享变量时, 如果存在写操作, 需要对共享变量加以保存, 因为可能存在多个线程同时修改共享变量或者在一个线程读取共享变量时另外一个变量在更新共享变量的情况, 而这些情况都可能会引起程序错误. 程序示例如下:
#include
#include
using namespace std;
#define COUNT 10000
int main()
{
int sum = 0;
#pragma omp parallel for shared(sum)
for (int i = 0; i < COUNT; i++)
{
sum = sum + i;
}
printf("%d\n", sum);
return 0;
}
//result=46367978
多次运行,结果可能不一样. 需要注意的是:循环迭代变量 i 在循环构造区域里是私有的。即使使用shared来修饰循环迭代变量,也不会改变循环迭代变量在循环构造区域中是私有的这一特点。程序示例如下:
#include
#include
using namespace std;
#define COUNT 10
int main()
{
int sum = 0;
int i = 0;
#pragma omp parallel for shared(sum, i)
for (i = 0; i < COUNT; i++)
{
sum = sum + i;
}
printf("%d\n", i);
printf("%d\n", sum);
return 0;
}
/*0
45*/
上述程序中,循环迭代变量i的输出值为0,尽管这里使用shared修饰变量i。注意,这里的规则只是针对循环并行区域,对于其他的并行区域没有这样的要求。同时在循环并行区域内,循环迭代变量是不可修改的。即在上述程序中,不能在for循环体内对循环迭代变量i进行修改。
#pragma omp parallel for shared(a)
for(i = 0; i < n; i++)
{
a[i] += i;
}
private子句用来指定哪些数据是线程私有的, 即每个线程具有变量的私有副本, 线程之间互不影响. 其语法形式为private(list), 使用方法如下:
// Author: Mario Talevski
#include
#include
using namespace std;
void test_private() {
int n = 8;
int i = 2, a = 3;
// i,a 定义为private之后不改变原先的值
#pragma omp parallel for private(i, a)
for (i = 0; i < n; i++)
{
a = i + 1;
printf("In for: thread %d has a value of a = %d for i = %d\n", omp_get_thread_num(), a, i);
}
printf("\n");
printf("Out for: thread %d has a value of a = %d for i = %d\n", omp_get_thread_num(), a, i);
}
int main(int argc, char* argv[])
{
test_private();
return 0;
}
/*
In for: thread 3 has a value of a = 4 for i = 3
In for: thread 0 has a value of a = 1 for i = 0
In for: thread 5 has a value of a = 6 for i = 5
In for: thread 2 has a value of a = 3 for i = 2
In for: thread 4 has a value of a = 5 for i = 4
In for: thread 1 has a value of a = 2 for i = 1
In for: thread 7 has a value of a = 8 for i = 7
In for: thread 6 has a value of a = 7 for i = 6
Out for: thread 0 has a value of a = 3 for i = 2*/
对于private子句中的变量, 需要注意一下两点:
lastprivate会在退出并行域时, 将其修饰变量的最后取值(last value)保存下来, 可以作用于 for 和 sections, 语法格式为lastprivate(list). 关于last value的定义: 如果是作用于for指令, 那么last value就是指串行执行的最后一次循环的值;如果是作用于sections指令, 那么last value就是执行完最后一个包含该变量的section之后的值. 使用方法如下:
#include
#include
using namespace std;
void test_last_private() {
int n = 8;
int i = 2, a = 3;
// lastprivate 将for中最后一次循环(i == n-1) a 的值赋给a
#pragma omp parallel for private(i) lastprivate(a)
for (i = 0; i < n; i++)
{
a = i + 1;
printf("In for: thread %d has a value of a = %d for i = %d\n", omp_get_thread_num(), a, i);
}
printf("\n");
printf("Out for: thread %d has a value of a = %d for i = %d\n", omp_get_thread_num(), a, i);
}
int main(int argc, char* argv[])
{
test_last_private();
return 0;
}
/*
In for: thread 2 has a value of a = 3 for i = 2
In for: thread 3 has a value of a = 4 for i = 3
In for: thread 5 has a value of a = 6 for i = 5
In for: thread 0 has a value of a = 1 for i = 0
In for: thread 6 has a value of a = 7 for i = 6
In for: thread 1 has a value of a = 2 for i = 1
In for: thread 4 has a value of a = 5 for i = 4
In for: thread 7 has a value of a = 8 for i = 7
Out for: thread 0 has a value of a = 8 for i = 2*/
firstprivate 子句用于为private变量提供初始值. 使用firstprivate修饰的变量会使用在前面定义的同名变量的值作为其初始值. 语法形式为firstprivate(list), 使用方法如下:
#include
#include
using namespace std;
void test_first_private() {
int n = 8;
int i = 0, a[8];
for (i = 0; i < n; i++) {
a[i] = i + 1;
}
#pragma omp parallel for private(i) firstprivate(a)
for (i = 0; i < n; i++)
{
printf("thread %d: a[%d] is %d\n", omp_get_thread_num(), i, a[i]);
}
}
int main(int argc, char* argv[])
{
test_first_private();
return 0;
}
/*
thread 3: a[3] is 4
thread 4: a[4] is 5
thread 0: a[0] is 1
thread 5: a[5] is 6
thread 2: a[2] is 3
thread 1: a[1] is 2
thread 6: a[6] is 7
thread 7: a[7] is 8
*/
下面是合并的例子
```cpp
#include
#include
using namespace std;
int main()
{
int k = 100;
#pragma omp parallel for firstprivate(k),lastprivate(k)
for (int i = 0; i < 3; i++)
{
k += i;
cout<<"kin"<<k<<endl;
}
cout << "kout " << k << endl;
}
/*
kinkin102100
kin101
kout 102*/
打印结果看出, 退出for循环的并行区域后,共享变量k的值变成了103,而不是保持原来的100不变。OpenMP规范中指出,如果是循环迭代,那么是将最后一次循环迭代中的值赋给对应的共享变量;如果是section构造,那么是最后一个section语句中的值赋给对应的共享变量。注意这里说的最后一个section是指程序语法上的最后一个,而不是实际运行时的最后一个运行完的。
default指定并行区域内变量的属性,C++的OpenMP中default的参数只能为shared或none。default(shared):表示并行区域内的共享变量在不指定的情况下都是shared属性
default(none):表示必须显式指定所有共享变量的数据属性,否则会报错,除非变量有明确的属性定义(比如循环并行区域的循环迭代变量只能是私有的)如果一个并行区域,没有使用default子句,那么其默认行为为default(shared)
用于取消任务分担结构(work-sharing constructs)中的隐式屏障(implicit barrier)(即并行区域中所有线程执行完毕之后,主线程才继续执行), 下面是一个使用示例:
#include
#include
using namespace std;
void test_nowait() {
int i, n = 3;
//#pragma omp parallel
{
//#pragma omp for nowait
for (i = 0; i < n; i++) {
printf("thread %d: ++++\n", omp_get_thread_num());
}
#pragma omp for
for (i = 0; i < n; i++) {
printf("thread %d: ----\n", omp_get_thread_num());
}
}
}
int main()
{
test_nowait();
}
/*
thread 3: ++++
thread 0: ++++
thread 0: ++++
thread 2: ++++
thread 1: ++++
thread 1: ++++
thread 0: ----
thread 0: ----
thread 3: ----
thread 1: ----
thread 1: ----
thread 2: ----
*/
因为for指令有一个隐式的屏障, 会同步所有的线程直到第一个for循环执行完, 再继续往下执行. 加上 nowait 之后就消除了这个屏障, 使线程执行完第一个for循环之后无需再等待其他线程就可以去执行第二个for循环的内容, 下面是加上nowait之后的输出:
#include
#include
using namespace std;
void test_nowait() {
int i, n = 3;
#pragma omp parallel
{
#pragma omp for nowait
for (i = 0; i < n; i++) {
printf("thread %d: ++++\n", omp_get_thread_num());
}
#pragma omp for
for (i = 0; i < n; i++) {
printf("thread %d: ----\n", omp_get_thread_num());
}
}
}
int main()
{
test_nowait();
}
/*
thread 0: ++++
thread 0: ----
thread 1: ++++
thread 1: ----
thread 2: ++++
thread 2: ----
*/
使用nowait时需要注意前后for之间有没有依赖关系, 如果第二个for循环需要用到第一个for循环的结果, 那么使用nowait就可能会造成程序错误.
schedule子句只作用于循环结构(loop construct), 它用来设置循环任务的调度方式. 语法形式为schedule(kind[,chunk_size])
, 其中kind的取值有static
, dynamic
, guided
,auto
,runtime
chunk_size是可选项,可以指定也可以不指定. 下面是使用方法:
#include
#include
using namespace std;
void test_schedule() {
int i, n = 10;
#pragma omp parallel for default(none) schedule(static, 2) private(i) shared(n)
for (i = 0; i < n; i++) {
printf("Iteration %d executed by thread %d\n", i, omp_get_thread_num());
}
}
int main()
{
test_schedule();
}
/*
Iteration 8 executed by thread 4
Iteration 9 executed by thread 4
Iteration 0 executed by thread 0
Iteration 1 executed by thread 0
Iteration 2 executed by thread 1
Iteration 3 executed by thread 1
Iteration 4 executed by thread 2
Iteration 5 executed by thread 2
Iteration 6 executed by thread 3
Iteration 7 executed by thread 3
*/
下面介绍一下各个取值的含义, 假设有n次循环, t个线程
静态调度, 如果不指定chunk_size , 那么会为每个线程分配 n/t 或者 n/t+1(不能除尽)次连续的迭代计算, 如果指定了 chunk_size, 那么每次为线程分配chunk_size次迭代计算, 如果第一轮没有分配完, 则循环进行下一轮分配, 假设n=8, t=4, 下表给出了chunk_size未指定、等于1、等于3时的分配情况.
(1) schedule(static, size)将所有迭代按每连续size个为一组,然后将这些组轮转分给各个线程。例如有4个线程,100次迭代,schedule(static, 5)将迭代:0-4, 5-9, 10-14, 15-19, 20-24…依次分给0, 1, 2, 3, 0…号线程。schedule(static)同schedule(static, size_av),其中size_av等于迭代次数除以线程数,即将迭代分成连续的和线程数相同的等分(或近似等分)。
(2) schedule(dynamic, size)同样分组,然后依次将每组分给目前空闲的线程(故叫动态)。
(3) schedule(guided, size) 把迭代分组,分配给目前空闲的线程,最初组大小为迭代数除以线程数,然后逐渐按指数方式(依次除以2)下降到size。
#include
#include
#include
#include
#include
using namespace std;
int counter = 10;
#pragma omp threadprivate(counter)
void test_static() {
#pragma omp parallel num_threads(3)
{
#pragma omp for
for (int i = 0; i < 9; i++) {
#pragma omp critical
cout << omp_get_thread_num() << ":" << i << " ";
}
}
}
int main() {
test_static();
}
/*
0:0 0:1 0:2 1:3 1:4 1:5 2:6 2:7 2:8*/
上面输出说明0号线程执行0-2迭代,1号执行3-5,2号执行6-9,相当于schedule(static, 3)。
我们将size换为1
void test_static() {
#pragma omp parallel num_threads(3)
{
#pragma omp for schedule(static, 1)
for (int i = 0; i < 9; i++) {
#pragma omp critical
cout << omp_get_thread_num() << ":" << i << " ";
}
}
}
int main() {
test_static();
}
/*
0:0 0:3 0:6 2:2 2:5 2:8 1:1 1:4 1:7*/
动态调度, 动态为线程分配迭代计算, 只要线程空闲就为其分配任务, 计算快的线程分配到更多的迭代. 如果不指定chunk_size参数, 则每次为一个线程分配一次迭代循环(相当于chunk_size=1), 若指定chunk_size, 则每次为一个线程分配chunk_size次迭代循环. 在动态调度下, 分配结果是不固定的, 重复执行同一个程序, 每次的分配结果一般来说是不同的, 下面给出n=12, t=4时, chunk_size未指定、等于2时的分配情况(运行两次)
使用动态动态可以一定程度减少负载不均衡的问题, 但是需要注意任务动态申请时也会有一定的开销.
guided调度是一种指定性的启发式自调度方法. 开始时每个线程会分配到较大的迭代块, 之后分配到的迭代块的大小会逐渐递减. 如果指定chunk_size, 则迭代块会按指数级下降到指定的chunk_size大小, 如果没有指定size参数, 那么迭代块大小最小会降到1(相当于chunk_size=1). 和动态调度一样, 执行块的线程会分到更多的任务, 不同的是这里迭代块的大小是变化的. 同样使用guided调度的分配结果也不是固定的, 重复执行会得到不同的分配结果. 下面给出n=20, t=4, chunk_size未指定、chunk_size=3时的分配情况(执行两次)
当设置chunk_size=3时, 因为最后只剩下18、19两次循环, 所以最后执行的那个线程只分配到2次循环.
下面的图展示了当循环次数为200次, 线程数量为4时, static 、 (dynamic,7) 、(guided, 7) 3种调度方式的分配情况
在for循环中, 如果每次循环之间花费的时间是不同的, 那么就可能出现负载不均衡问题,比如,
static : use time 8.67s
static,2 : use time 6.42s
dynamic : use time 5.62s
dynamic,2: use time 6.43s
guided : use time 5.92s
guided,2 : use time 6.43s
对于static调度, 如果不指定chunk_size的值, 则会将最后几次循环分给最后一个线程, 而最后几次循环是最耗时的, 其他线程执行完各自的工作需要等待这个线程执行完毕, 浪费了系统资源, 这就造成了负载不均衡. dynamic和guided可以在一定程度上减轻负载不均衡, 但是也不是绝对的, 最终选用哪种方式还是要根据具体的问题.
同步指令主要用来控制多个线程之间对于共享变量的访问. 它可以保证线程以一定的顺序更新共享变量, 或者保证两个或多个线程不同时修改共享变量.
同步路障(barrier), 当线程遇到路障时必须要停下等待, 直到并行区域中的所有线程都到达路障点, 线程才继续往下执行. 在每一个并行域和任务分担域的结束处都会有一个隐式的同步路障, 即在parallel、for、sections、single构造的区域之后会有一个隐式的路障, 因此在很多时候我们无需显示的插入路障. 下面示例:
#include
#include
using namespace std;
void test_barrier() {
#pragma omp parallel num_threads(6)
{
#pragma omp critical
cout << omp_get_thread_num() << " ";
#pragma omp critical
cout << omp_get_thread_num() + 10 << " ";
}
}
int main() {
test_barrier();
}
/*
0 10 3 13 4 14 2 12 5 15 1 11*/
我们加上barrier
#include
#include
using namespace std;
void test_barrier() {
#pragma omp parallel num_threads(6)
{
#pragma omp critical
cout << omp_get_thread_num() << " ";
#pragma omp barrier
#pragma omp critical
cout << omp_get_thread_num() + 10 << " ";
}
}
int main() {
test_barrier();
}
/*
4 1 2 0 3 5 15 11 13 12 14 10*/
可以看到,这时一位数数字打印完了才开始打印两位数数字,因为,所有线程执行到第5行代码时,都要等待所有线程都执行到第5行,这时所有线程再都继续执行第7行及以后的代码,即所谓同步.
再来说说for, sections, single directives的隐含barrier,以及nowait clause如下示例:
#include
#include
#include
#include
#include
using namespace std;
void test_barrier() {
#pragma omp parallel num_threads(6)
{
#pragma omp for
for (int i = 0; i < 10; ++i) {
#pragma omp critical
cout << omp_get_thread_num() << " ";
}
//this is an implicit barrier here
#pragma omp critical
cout << omp_get_thread_num() + 10 << " ";
}
}
int main() {
test_barrier();
}
/*
0 0 3 3 1 1 5 4 2 2 14 12 13 15 11 10*/
加上nowait将会disable implicit barrier
#include
#include
#include
#include
#include
using namespace std;
void test_barrier() {
#pragma omp parallel num_threads(6)
{
#pragma omp for nowait
for (int i = 0; i < 10; ++i) {
#pragma omp critical
cout << omp_get_thread_num() << " ";
}
//The implicit barrier here is disabled by nowait.
#pragma omp critical
cout << omp_get_thread_num() + 10 << " ";
}
}
int main() {
test_barrier();
}
/*
0 0 10 2 2 12 1 1 11 5 15 3 3 13 4 14*/
sections, single directives是类似的.
ordered结构允许在并行域中以串行的顺序执行一段代码, 如果我们在并行域中想按照顺序打印被不同的线程计算的数据, 就可以使用这个子句. 使用在有ordered clause的for directive(或parallel for)中,确保代码将被按迭代次序执行(像串行程序一样).
在使用时注意以下两点
#pragma omp parallel for ordered
#include
#include
using namespace std;
void test_ordered() {
#pragma omp parallel num_threads(8)
{
#pragma omp for ordered
for (int i = 0; i < 10; i++) {
#pragma omp critical
cout << i << " ";
#pragma omp ordered
{
#pragma omp critical
cout << -i << " ";
}
}
}
}
int main() {
test_ordered();
}
/*
4 0 0 1 -1 7 8 9 6 5 2 -2 3 -3 -4 -5 -6 -7 -8 -9
*/
只看前面有"-“的数字,是不是按顺序的,而没有”-"的数字则没有顺序。值得强调的是for directive的ordered clause只是配合ordered directive使用,而不是让迭代有序执行的意思
另一个例子
#include
#include
using namespace std;
void test_order() {
int i, tid, n = 5;
int a[5];
for (i = 0; i < n; i++) {
a[i] = 0;
}
#pragma omp parallel for default(none) ordered schedule(dynamic) private (i, tid) shared(n, a)
for (i = 0; i < n; i++) {
tid = omp_get_thread_num();
printf("Thread %d updates a[%d]\n", tid, i);
a[i] += i;
#pragma omp ordered
{
printf("Thread %d printf value of a[%d] = %d\n", tid, i, a[i]);
}
}
}
int main() {
test_order();
}
/*
Thread 0 updates a[0]
Thread 0 printf value of a[0] = 0
Thread 4 updates a[2]
Thread 3 updates a[4]
Thread 1 updates a[3]
Thread 2 updates a[1]
Thread 2 printf value of a[1] = 1
Thread 4 printf value of a[2] = 2
Thread 1 printf value of a[3] = 3
Thread 3 printf value of a[4] = 4
*/
从输出结果我们可以看到, 在update时是以乱序的顺序更新, 但是在打印时是以串行顺序的形式打印.
临界区(critical), 临界区保证在任意一个时间段内只有一个线程执行该区域中的代码, 一个线程要进入临界区必须要等待临界区处于空闲状态, 下面是语法形式
#pragma omp critical [(name)]
structured block
其中name是为临界区指定的一个名字.
void test_critical() {
#pragma omp parallel num_threads(6)
{
cout << omp_get_thread_num() << omp_get_thread_num();
}
}
int main() {
test_critical();
}
/*
001152254433
*/
上面5号线程代码时被2号线程打断了(并不是每次运行都可能出现打断)。
void test_critical() {
#pragma omp parallel num_threads(6)
{
#pragma omp critical
cout << omp_get_thread_num() << omp_get_thread_num();
}
}
int main() {
test_critical();
}
/*
001133224455
*/
这次不管运行多少遍都不会出现某个数字不是连续两个出现,因为在第4行代码被一个线程执行期间,其他线程不能执行(该行代码是临界区)。
下面是一个求和的使用示例, 注意这里只是用来说明临界区的作用, 对于求和操作我们可以使用reduction指令
#include
#include
#include
#include
#include
using namespace std;
void test_critical() {
int n = 100, sum = 0, sumLocal, i, tid;
int a[100];
for (i = 0; i < n; i++) {
a[i] = i;
}
#pragma omp parallel shared(n, a, sum) private (tid, sumLocal)
{
tid = omp_get_thread_num();
sumLocal = 0;
#pragma omp for
for (i = 0; i < n; i++) {
sumLocal += a[i];
}
#pragma omp critical(update_sum)
{
sum += sumLocal;
printf("Thread %d: sumLocal = %d sum =%d\n", tid, sumLocal, sum);
}
}
printf("Value of sum after parallel region: %d\n", sum);
}
int main() {
test_critical();
}
/*
Thread 6: sumLocal = 978 sum =978
Thread 0: sumLocal = 78 sum =1056
Thread 2: sumLocal = 416 sum =1472
Thread 1: sumLocal = 247 sum =1719
Thread 4: sumLocal = 690 sum =2409
Thread 5: sumLocal = 834 sum =3243
Thread 3: sumLocal = 585 sum =3828
Thread 7: sumLocal = 1122 sum =4950
Value of sum after parallel region: 4950
下面是将临界区去掉的运行结果(运行结果不是固定的, 这里只是其中一种情况):
Thread 5: sumLocal = 834 sum =834
Thread 2: sumLocal = 416 sum =3531
Thread 6: sumLocal = 978 sum =3115
Thread 1: sumLocal = 247 sum =247
Thread 7: sumLocal = 1122 sum =1447
Thread 3: sumLocal = 585 sum =4116
Thread 0: sumLocal = 78 sum =325
Thread 4: sumLocal = 690 sum =2137
Value of sum after parallel region: 4116
*/
通过对比我们可以看到临界区保证了程序的正确性.
原子操作, 可以锁定一个特殊的存储单元(可以是一个单独的变量,也可以是数组元素), 使得该存储单元只能原子的更新, 而不允许让多个线程同时去写(是不是很像critical directive). atomic只能作用于单条赋值语句, 而不能作用于代码块. 语法形式为:
#pragma omp atomic
statement
在C/C++中, statement必须是下列形式之一
x++, x--, ++x, --x
,x binop= expr
其中binop是二元操作符:+, -, *, /, &, ^, |, <<, >>
之一int m = 0;
void test_automic() {
#pragma omp parallel num_threads(6)
{
for (int i = 0; i < 1000000; i++) {
++m;
}
}
cout << "The expect value:" << 6000000<< endl;
cout << "The value is " << m << endl;
}
int main() {
test_automic();
}
/*
The expect value:6000000
The value is 1223186
*/
m实际值比预期要小,因为“++m”的汇编代码不止一条指令,假设三条:load, inc, mov(读RAM到寄存器、加1,写回RAM),有可能线程A执行到inc时,线程B执行了load(线程A inc后的值还没写回),接着线程A mov,线程B inc后再mov,原本应该加2就变成了加1.
使用atomic directive后可以得到正确结果:
int m = 0;
void test_automic() {
#pragma omp parallel num_threads(6)
{
for (int i = 0; i < 1000000; i++) {
#pragma omp atomic
++m;
}
}
cout << "The expect value:" << 6000000<< endl;
cout << "The value is " << m << endl;
}
int main() {
test_automic();
}
/*
The expect value:6000000
The value is 6000000
*/
那当我们使用critical 会怎样呢
int m = 0;
void test_automic() {
#pragma omp parallel num_threads(6)
{
for (int i = 0; i < 1000000; i++) {
#pragma omp critical
++m;
}
}
cout << "The expect value:" << 6000000<< endl;
cout << "The value is " << m << endl;
}
int main() {
test_automic();
}
/*
The expect value:6000000
The value is 6000000
*/
差别为何呢,显然是效率。atomic的速度比critical快
下面是另外一个例子
#include
#include
using namespace std;
void test_atomic() {
int counter = 0, n = 1000000, i;
#pragma omp parallel for shared(counter, n)
for (i = 0; i < n; i++) {
#pragma omp atomic
counter += 1;
}
printf("counter is %d\n", counter);
}
int main() {
test_atomic();
}
/*
counter is 1000000
*/
对于下面的情况
#pragma omp atomic
ic += func();
atomic只保证ic的更新是原子的, 即不会被多个线程同时更新, 但是不会保证func函数的执行是原子的, 即多个线程可以同时执行func函数, 如果要使func的执行也是原子的, 可以使用临界区.
用于指定一段代码只由主线程执行. master指令和single指令的区别如下:
#include
#include
using namespace std;
void test_master() {
int a, i, n = 5;
int b[5];
#pragma omp parallel shared(a, b) private(i)
{
#pragma omp master
{
a = 10;
printf("Master construct is executed by thread %d\n", omp_get_thread_num());
}
#pragma omp barrier
#pragma omp for
for (i = 0; i < n; i++)
b[i] = a;
}
printf("After the parallel region:\n");
for (i = 0; i < n; i++)
printf("b[%d] = %d\n", i, b[i]);
}
int main() {
test_master();
}
/*
Master construct is executed by thread 0
After the parallel region:
b[0] = 10
b[1] = 10
b[2] = 10
b[3] = 10
b[4] = 10
*/
flush指令主要用于处理内存一致性问题. 每个处理器(processor)都有自己的本地(local)存储单元:寄存器和缓存, 当一个线程更新了共享变量之后, 新的值会首先存储到寄存器中, 然后更新到本地缓存中. 这些更新并非立刻就可以被其他线程得知, 因此在其它处理器中运行的线程不能访问这些存储单元. 如果一个线程不知道这些更新而使用共享变量的旧值进行运算, 就可能会得到错误的结果.
通过使用flush指令, 可以保证线程读取到的共享变量的最新值. 下面是语法形式:
#pragma omp flush[(list)]
list指定需要flush的共享变量, 如果不指定list, 将flush作用于所有的共享变量. 在下面的几个位置已经隐式的添加了不指定list的flush指令.
threadprivate作用于全局变量, 用来指定该全局变量被各个线程各自复制一份私有的拷贝, 即各个线程具有各自私有、线程范围内的全局对象, 语法形式如下:
#pragma omp threadprivate(list)
其与private不同的时, threadprivate变量是存储在heap或者Thread local storage当中, 可以跨并行域访问, 而private绝大多数情况是存储在stack中, 只在当前并行域中访问, 下面是一个使用示例:
#include
#include
using namespace std;
void test_threadprivate() {
int a;
cout << omp_get_thread_num() << ": " << &a << endl;
#pragma omp parallel num_threads(8)
{
int b;
#pragma omp critical
cout << omp_get_thread_num() << " : " << &a << " " << &b << endl;
}
}
int main() {
test_threadprivate();
}
/*
0: 0000007B7F0FFC04
0 : 0000007B7F0FFC04 0000007B7F0FF924
3 : 0000007B7F0FFC04 0000007B7F5FFC14
4 : 0000007B7F0FFC04 0000007B7F6FF954
2 : 0000007B7F0FFC04 0000007B7F4FFA04
1 : 0000007B7F0FFC04 0000007B7F1FF784
6 : 0000007B7F0FFC04 0000007B7F8FFC94
7 : 0000007B7F0FFC04 0000007B7F9FFAB4
5 : 0000007B7F0FFC04 0000007B7F7FFA64
*/
要被8个线程执行8遍,变量a是线程之间共享的,变量b是每个线程都有一个(在线程自己的栈空间).
怎么区分哪些变量是共享的,哪些是私有的呢。在parallel region内定义的变量(非堆分配)当然是私有的。没有特别用clause指定的(上面代码就是这样),在parallel region前(parallel region后的不可见,这点和纯C++相同)定义的变量是共享的,在堆(用new或malloc函数分配的)上分配的变量是共享的(即使是在多个线程中使用new或malloc, 当然指向这块堆内存的指针可能是私有的), for directive作用的C++ for的循环变量不管在哪里定义都是私有的.
回到threadprivate directive,看例子:
#include
#include
using namespace std;
int a;
#pragma omp threadprivate(a)
void test_threadprivate() {
cout << omp_get_thread_num() << ": " << &a << endl;
#pragma omp parallel num_threads(8)
{
int b;
#pragma omp critical
cout << omp_get_thread_num() << " : " << &a << " " << &b << endl;
}
}
int main() {
test_threadprivate();
}
/*
0: 000002A334B03BF4
0 : 000002A334B03BF4 000000AE3A13F364
2 : 000002A334B17AA4 000000AE3A7FFB14
1 : 000002A334B17CC4 000000AE3A4FF7F4
4 : 000002A334B17EE4 000000AE3A9FF5D4
3 : 000002A334B1A494 000000AE3A8FF834
5 : 000002A334B1DEC4 000000AE3AAFFA14
6 : 000002A334B1FCB4 000000AE3ABFF5E4
7 : 000002A334B1FA94 000000AE3ACFF734
*/
下面另外一个示例
int counter;
#pragma omp threadprivate(counter)
void test_threadprivate() {
#pragma omp parallel num_threads(3)
{
counter = omp_get_thread_num();
printf("1: thread %d : counter is %d\n", omp_get_thread_num(), counter);
}
printf("\n");
#pragma omp parallel num_threads(3)
{
printf("2: thread %d : counter is %d\n", omp_get_thread_num(), counter);
}
}
int main() {
test_threadprivate();
}
/*
1: thread 0 : counter is 0
1: thread 2 : counter is 2
1: thread 1 : counter is 1
2: thread 0 : counter is 0
2: thread 2 : counter is 2
2: thread 1 : counter is 1
*/
从输出结果我们可以看到, 在第二个并行域中, counter保存了在第一个并行域中的值. 如果要使两个并行域之间可以共享threadprivate变量的值, 需要满足以下几个条件:
任意一个并行域都不能嵌套在其他并行域中
执行两个并行域的线程数量要相同
执行两个并行域时的线程亲和度策略要相同
在进入并行域之前dyn-var变量的值必须为false(0).
用来控制并行域是串行执行还是并行执行, 只能作用于paralle指令, 下面是其语法形式:
#pragma omp parallel if(scalar-logical-expression)
如果if的判断条件为true, 则并行执行, 否则串行执行, 下面是一个使用示例
#include
#include
#include
#include
#include
using namespace std;
void test_if() {
int n = 1, tid;
printf("n = 1\n");
#pragma omp parallel if(n>5) default(none) private(tid) shared(n)
{
tid = omp_get_thread_num();
printf("thread %d is running\n", tid);
}
printf("\n");
n = 10;
printf("n = 10\n");
#pragma omp parallel if(n>5) default(none) private(tid) shared(n)
{
tid = omp_get_thread_num();
printf("thread %d is running\n", tid);
}
}
int main() {
test_if();
}
/*
n = 1
thread 0 is running
n = 10
thread 0 is running
thread 4 is running
thread 2 is running
thread 6 is running
thread 7 is running
thread 5 is running
thread 1 is running
thread 3 is running
*/
如果利用循环, 将某项计算的所有结果进行求和(或者减、乘等其他操作)得出一个数值, 这在并行计算中十分常见, 通常将其称为规约. OpenMP提供了reduction子句由于规约操作, 其语法形式为
reduction(operator:list)
下面是一个实例
#include
#include
#include
#include
#include
using namespace std;
void test_reduction() {
int sum = 0;
int i;
int n = 100;
int a[100];
for (i = 0; i < n; i++) {
a[i] = i;
}
#pragma omp parallel for default(none) private(i) shared(a,n) reduction(+:sum)
for (i = 0; i < n; i++) {
sum += a[i];
}
printf("sum is %d\n", sum);
}
int main() {
test_reduction();
}
/*
sum is 4950
*/
使用规约子句之后, 无需再对sum进行保护, 下面是reduction支持的操作符以及变量的初值
将主线程中threadprivate变量的值复制到执行并行域的各个线程的threadprivate变量中, 作为各线程中threadprivate变量的初始值. 作用于parallel指令, 下面是一个使用示例:
#include
#include
#include
#include
#include
using namespace std;
int counter = 10;
#pragma omp threadprivate(counter)
void test_copyin() {
printf("counter is %d\n", counter);
#pragma omp parallel copyin(counter)
{
counter = omp_get_thread_num() + counter + 1;
printf(" thread %d : counter is %d\n", omp_get_thread_num(), counter);
}
printf("counter is %d\n", counter);
}
int main() {
test_copyin();
}
/*
counter is 10
thread 4 : counter is 15
thread 5 : counter is 16
thread 7 : counter is 18
thread 3 : counter is 14
thread 1 : counter is 12
thread 6 : counter is 17
thread 2 : counter is 13
thread 0 : counter is 11
counter is 11
*/
copyprivate:配合single指令,将指定线程的专有变量广播到并行域内其他线程的同名变量中;
copyin n:用来指定一个threadprivate类型的变量需要用主线程同名变量进行初始化;
将一个线程私有变量的值广播到执行同一并行域的其他线程. 只能作用于single指令, 下面是一个使用示例:
#include
#include
#include
#include
#include
using namespace std;
int counter = 10;
#pragma omp threadprivate(counter)
void test_copyprivate() {
int i;
#pragma omp parallel private(i)
{
#pragma omp single copyprivate(i, counter)
{
i = 50;
counter = 100;
printf("thread %d execute single\n", omp_get_thread_num());
}
printf("thread %d: i is %d and counter is %d\n", omp_get_thread_num(), i, counter);
}
}
int main() {
test_copyprivate();
}
/*
thread 3 execute single
thread 5: i is 50 and counter is 100
thread 3: i is 50 and counter is 100
thread 6: i is 50 and counter is 100
thread 4: i is 50 and counter is 100
thread 7: i is 50 and counter is 100
thread 0: i is 50 and counter is 100
thread 1: i is 50 and counter is 100
thread 2: i is 50 and counter is 100
*/
除上述编译制导指令之外,OpenMP还提供了一组API函数用于控制并发线程的某些行为,下面是一些常用的OpenMP API函数以及说明:
// 设置并行线程数
_OMPIMP void _OMPAPI omp_set_num_threads(int _Num_threads);
// 获取当前并行线程数
_OMPIMP int _OMPAPI omp_get_num_threads(void);
// 获取当前系统最大可并行运行的线程数
_OMPIMP int _OMPAPI omp_get_max_threads(void);
// 获取当前运行线程的ID,注意和操作系统中的线程ID不同
_OMPIMP int _OMPAPI omp_get_thread_num(void);
// 获取当前系统中处理器数目
_OMPIMP int _OMPAPI omp_get_num_procs(void);
_OMPIMP void _OMPAPI omp_set_dynamic(int _Dynamic_threads);
_OMPIMP int _OMPAPI omp_get_dynamic(void);
_OMPIMP int _OMPAPI omp_in_parallel(void);
_OMPIMP void _OMPAPI omp_set_nested(int _Nested);
_OMPIMP int _OMPAPI omp_get_nested(void);
_OMPIMP void _OMPAPI omp_init_lock(omp_lock_t * _Lock);
_OMPIMP void _OMPAPI omp_destroy_lock(omp_lock_t * _Lock);
_OMPIMP void _OMPAPI omp_set_lock(omp_lock_t * _Lock);
_OMPIMP void _OMPAPI omp_unset_lock(omp_lock_t * _Lock);
_OMPIMP int _OMPAPI omp_test_lock(omp_lock_t * _Lock);
_OMPIMP void _OMPAPI omp_init_nest_lock(omp_nest_lock_t * _Lock);
_OMPIMP void _OMPAPI omp_destroy_nest_lock(omp_nest_lock_t * _Lock);
_OMPIMP void _OMPAPI omp_set_nest_lock(omp_nest_lock_t * _Lock);
_OMPIMP void _OMPAPI omp_unset_nest_lock(omp_nest_lock_t * _Lock);
_OMPIMP int _OMPAPI omp_test_nest_lock(omp_nest_lock_t * _Lock);
_OMPIMP double _OMPAPI omp_get_wtime(void);
_OMPIMP double _OMPAPI omp_get_wtick(void);
使用上述函数的一些例子
#include
#include
using namespace std;
void TestAPIs()
{
// 获取当前并行线程数
cout << "Num of Procs: " << omp_get_num_procs() << endl;
// 获取当前系统最大可并行运行的线程数
cout << "Max Threads: " << omp_get_max_threads() << endl;
cout << "Set Num of Threads = 2 " << endl;
// 设置并行线程数
omp_set_num_threads(2);
#pragma omp parallel
cout << "Get Thread Num: " << omp_get_thread_num() << endl;
// 设置并行线程数
omp_set_num_threads(omp_get_num_procs() - 1);
#pragma omp parallel
{
cout << "OPENMP\n";
}
}
int main()
{
TestAPIs();
}
OpenMP提供了一些环境变量,用来在运行时对并行代码的执行进行控制。这些环境变量可以控制:1)设置线程数;2)指定循环如何划分;3)将线程绑定到处理器;4)启用/禁用嵌套并行,设置最大的嵌套并行级别;5)启用/禁用动态线程;6)设置线程堆栈大小;7)设置线程等待策略。常用的环境变量:
OMP_SCHEDULE
:用于for循环并行化后的调度,它的值就是循环调度的类型;
OMP_NUM_THREADS
:用于设置并行域中的线程数;
OMP_DYNAMIC
:通过设定变量值,来确定是否允许动态设定并行域内的线程数;
OMP_NESTED
:指出是否可以并行嵌套。