开放多处理过程(Open Multi-Processing, OpenMP)属于共享内存的并行编程模型。
注意这里fork的东西是任务(task)或纤程(fiber/lightweight thread),而不是线程
OpenMP提供了三种API
#pragma omp <directive-name> [clause,...]
常用API
omp_get_thread_num()
:线程idomp_get/set_num_procs
:使用的物理核数目omp_get/set_num_threads
:使用的线程数目
OMP_NUM_THREADS
作为环境变量#pragma omp parallel for
:循环内不能含跳转、跳出指令,且循环次数应确定
parallel
(SPMD编程模型),则循环只是被简单拷贝,循环体执行多次parallel for
,则会触发调度器,内层循环不会被拷贝omp_set_nested(1)
打开#pragma omp ... private (<variable list>)
:将变量声明为私有变量
firstprivate(<var>)
继承之前定义的值
var
是基本数据类型:直接拷贝var
是数组:拷贝sizeof(var)
大小到私有内存空间var
是指针:指向同个共享var
var
是一个类:调用拷贝构造函数构造私有副本lastprivate(<var>)
返回最后的值至全局single
:单线程执行,如处理IO
nowait
master
:仅对主线程进行操作tid = omp_get_thread_num();
if (tid == 0) {
nthreads = omp_get_num_threads();
printf("Number of threads = %d\n", nthreads);
}
// 等价于
#pragma omp master
// 近似等价于,在嵌套并行时会有区别
#pragma omp single nowait
语法限制
!=
作为循环终止判断条件
for(int i = 0; i != 8; ++i)
sections
和section
:
sections
作用域内所有的section
都可以并行执行section
可以被不同线程执行,一个线程可以执行多个section
reduction (op:list)
+,-,*,&,|,&&,||
// Example 1
int i, j, k;
float **a, **b;
// initialize b[][] as 1-hop distance matrix
for (k = 0; k < N; ++k)
#pragma omp parallel for private(j)\
num_threads(8)
for (i = 0; i < N; ++i)
for (j = 0; j < M; ++j)
a[i][j] = min(a[i][j], b[i][k] + b[k][j]);
// copy a[][] to b[][]
// Example 2
#pragma omp parallel shared(a,b,c,d) private(i)
{
#pragma omp sections
{
#pragma omp section
{
for (i = 0; i < N; ++i)
c[i] = a[i] + b[i];
}
#pragma omp section
{
for (i = 0; i < N; ++i)
d[i] = a[i] * b[i];
}
} /* end of sections */
}/* end of parallel section */
// Example 3
float sum(const float *a, size_t n)
{
float total = 0.;
#pragma omp parallel for reduction(+:total)
for (size_t i = 0; i < n; i++) {
total += a[i];
}
return total;
}
数据依赖性
a[i] = b[i+1]
a[i] = b[i]
竞态(RC)问题出现的原因:多个线程访问同一共享变量的不确定性
避免RC问题的方法:
private
语句,在线程函数内声明变量,分配线程栈下面这种机制由于判断flag
和flag
置位是两个操作,故并不能保证互斥,需要有原子性的TS(test-and-set)操作,或者锁
// initialize flag = 0
while(flag != 0) /* wait */;
flag = 1;
barrier
:同步,类似于join,会被自动添加在worksharing结构中,如for
和single
;可以通过nowait
取消critical
:临界区,保证互斥(只有一个线程可以进入),都会变成串行执行atomic
:没有冲突的部分会被并行执行,只对单一简单二元语句有用#pragma omp parallel for
{
for (i = 0; i < n; ++i){
#pragma omp critical
// #pragma omp atomic
x[index[i]] += WorkOne(i);
y[i] += WorkTwo(i);
}
}
// critial保护:WorkOne的调用、index[i]的寻找、自加、赋值
// atomic保护:加法和赋值
schedule(static[,chunk])
:相当于按照chunk大小循环展开,round-robin,低开销,但会导致负载不均schedule(dynamic[,chunk])
:也是按chunk大小循环展开,高开销,但能负载均衡(work-stealing的感觉)schedule(guided[,chunk])
:指导性的启发式自调度方法。开始时每个线程会分配到较大的迭代块,之后分配到的迭代块会逐渐递减。迭代块的大小会按指数级下降到指定的chunk大小,如果没有指定chunk参数,那么迭代块大小最小会降到1。多核意味着有多个cache,就会有cache一致性问题:一个核cache的写会导致另一个核cache的失效
经验原则(rule of thumb):
一个多核程序具有好的局部性是指一个核的内存写不会与其他核的进行交互。 比如说123123123就不是好的分配方式,而111222333就是较好的局部性。
#include <omp.h>
g++ -O3 -fopenmp <source>
实际上OpenMP的编译器就是将OpenMP的语法转为pthreads