0%

共享内存系统并行 OpenMP

OpenMP是一个针对共享内存并行编程的API。

与之前的MPI不同的是,OpenMP是线程级并行,比MPI的进程级并行要更轻量化一些。在

更重要的一个特点是,MPI的并行需要完全重写整个程序,而将一个串行程序改造成OpenMP的并行则有可能只要进行少量的改动即可。

而且gcc原生支持OpenMP,不需要像MPI一样另外要装个运行环境和运行库。

用gcc编译时加上-fopenmp开关即可:

1
2
3
$ gcc -fopenmp <source.c> -o <exec>

$ g++ -fopenmp <source.cpp> -o <exec>

示例

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
27
28
29
30
31
/* ***********************************************
MYID : Chen Fan
LANG : G++
PROG : openmp_test
************************************************ */

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <omp.h>

using namespace std;

void hello()
{
int my_rank = omp_get_thread_num();
int thread_count = omp_get_num_threads();

printf("Hello from thread %d of %d\n", my_rank, thread_count);
}

int main()
{
int thread_count = 4;

# pragma omp parallel num_threads(thread_count)
hello();

return 0;
}

上面这段示例代码中首先增加了一个omp.h头文件,然后就是主函数中多出来一句串行代码中没有的预处理器指令,其他的跟正常的串行程序没什么区别。

# pragma是C/C++中用以允许非C语言规范部分的行为,如果编译器不支持预处理器指令,那么编译时这句话就会被忽略掉。

OpenMP就依靠这些以# pragma omp开头的预处理器指令来进行线程级并行。

预处理器指令后面加的是一些子句,用来附加额外的控制信息。比如说num_threads()子句是用来控制接下来的代码块中需要用多少个线程进行并行。

让我比较头疼的是…虽然说并行的几个线程可以认为是同时在运行,当他们都要占用屏幕进行内容输出时会发生抢占。然而MPI的进程在相互抢占时,至少还能保证一个进程输出的东西是在一起的,只不过每次运行时,进程输出的顺序可能会不同。但是OpenMP中,上面那个程序输出来的东西就几乎是完全混乱的了…0.0…应该是其中一个线程只输出了几个字符就被另一个线程抢占了。

OpenMP的并行模式

在MPI中,程序编译完成之后如果直接打开是无法运行的,需要用mpiexec来调用生成好的可执行文件,mpiexec会首先得到运行的目标机器、进程数等等情况,然后开始启动多个进程,等到多进程全部开起来之后,并行就开始了。

而在OpenMP中,编译完成之后的可执行文件可以直接运行,程序在一开始是串行运行,到了需要并行的时候,单进程单线程会分裂成单进程多线程(其实是除了主线程以外,又启动了几个新的线程同时执行),执行完毕之后又回到单线程的串行。而且每次并行的线程数是可以在运行时指定的。

比如说像这张图:

MPI和OpenMP的区别还是比较大的。

所以相对来说,OpenMP可以只把其中的一部分作并行处理,而且并行的时候共享的内存、变量等等都是在一起的,从串行程序的基础上改造过来非常容易,可能只要加几段预处理器指令就可以了,剩下的交给编译器和处理器去解决就可以了。

冲突解决

不同于MPI需要依靠进程间通信来完成协作,既然OpenMP是内存共享的,很多操作只需要解决掉对同一块内存的访问冲突就可以多线程协作了。

OpenMP中的冲突解决主要有四种方法:

  • Crirical指令/归约指令

例如:

1
2
3
4
5
6
7
	int sum = 0;
# pragma omp parallel for num_threads(100)
for (int i=0;i<100;i++)
{
sum += i;
}
printf("%d\n", sum);

直接运行的结果是每次运行,sum最终给出来的结果都有可能是不同的。因为运行时多个线程同时访问了sum这个变量,可能前一个线程写上去的内容马上被下一个线程给覆盖掉了,即出现了数据冲突

1
2
3
4
5
6
7
8
	int sum = 0;
# pragma omp parallel for num_threads(100)
for (int i=0;i<100;i++)
{
# pragma omp critical
sum += i;
}
printf("%d\n", sum);

加上# pragma omp critical指令即告诉编译器需要安排线程对下面执行的代码进行互斥访问,即每次只能够有一个线程执行下面的这一句代码。

或者采用:

1
2
3
4
5
6
7
	int sum = 0;
# pragma omp parallel for num_threads(100) reduction(+: sum)
for (int i=0;i<100;i++)
{
sum += i;
}
printf("%d\n", sum);

reduction(+: sum)是归约子句,加上这一句之后,执行下面的并行任务时,sum本身是共享的,但每个线程在执行时都会产生一个私有变量,当并行块运算结束之后再将私有变量的值整合回共享变量。

  • 带命名的critical指令

可用# pragma omp critical(name)来命名不同的临界区

对同一个临界区的访问还是跟上面一样,一次只有一个进程能够进行操作,而对不同的临界区则可以有不同的进程进行同时访问。

  • atomic指令

# pragma omp atomic作用在形式为:

1
2
3
4
5
x <op>= <expression>;
x++;
++x;
x--;
--x;

的指令中。

这些语句可以用CPU中的某些特殊硬件指令来实现。

  • 简单锁
1
2
3
omp_set_lock(&lock);
critical section
omp_unset_lock(&lock);

锁住的区域只允许单个线程进行访问