一、基本概念
OpenMP是一种用于共享内存并行系统的多线程程序设计方案,支持的编程语言包括C、C++和Fortran。OpenMP提供了对并行算法的高层抽象描述,特别适合在多核CPU机器上的并行程序设计。编译器根据程序中添加的pragma指令,自动将程序并行处理,使用OpenMP降低了并行编程的难度和复杂度。
执行模式
OpenMP采用fork-join的执行模式。开始的时候只存在一个主线程,当需要进行并行计算的时候,派生出若干个分支线程来执行并行任务。当并行代码执行完成之后,分支线程会合,并把控制流程交给单独的主线程。
OpenMP编程模型以线程为基础,通过编译制导指令制导并行化,有三种编程要素可以实现并行化控制,他们分别是编译指导语句、API函数集和环境变量。
编译指导语句
#pragma omp directive_name [clause, ...]
#pragma omp | directive-name | [clause, …] |
---|---|---|
指导指令前缀所有的OpenMP语句都有前缀。 | 指导指令。每个语句都有一个指导指令,指导编译器并行化 | 子句,用来控制具体行为,任选、无序。 |
并行区域与任务分配
并行区域(创建线程): parallel
任务分配: for,sections,single,master
二、编译器指令
parallel命令
用来构造一个并行域,并行域中的代码被所有的线程执行.
格式 : #pragma omp parallel [子句[子句]…]
创建一组线程,主线程的线程号为0。
从并行域开始,代码被复制并被所有线程执行。
并行域结束时有个隐式路障,只有主线程能在此之后继续执行。
int main()
{
#pragma omp parallel
{
printf("hello world!\n");
}
}
指定线程数量
num_threads子句的设置
omp_set_num_threads() 库函数
OMP_NUM_THREADS 环境变量
默认——计算机的逻辑CPU数量。
int main()
{
#pragma omp parallel num_threads(8)
{
printf("hello world!, ThreadId = % d\n", omp_get_thread_num());
}
}
if子句
if子句的值来决定是否并行执行。
int main()
{
int n = 12;
#pragma omp parallel if(n>10) num_threads(2)
{
printf("if clause, ThreadId = % d\n", omp_get_thread_num());
}
}
动态设置线程数量
计算线程数量:
1、每个线程的循环次数较少时,线程创建代价较大;
2、当线程数量远大于CPU数量时,将产生大量的线程切换、
调度。
每个线程运行的循环次数不低于4次。总的运行线程数最大不超过2倍CPU核数。
const int MIN_ITERATOR_NUM = 4;
const int g_ncore = omp_get_num_procs(); //获取执行核的数量
int dtn(int n, int min_n)
{
int max_tn = n / min_n;
int tn = max_tn > g_ncore ? g_ncore : max_tn;
//tn表示要设置的线程数量
if (tn < 1)
{
tn = 1;
}
return tn;
}
int main()
{
int n = 50;
int max_tn = n / MIN_ITERATOR_NUM;
int tn = max_tn > 2 *g_ncore ? 2 * g_ncore : max_tn;//tn表示要设置的线程数量
#pragma omp parallel for num_threads(dtn(n, MIN_ITERATOR_NUM))
for (int i = 0; i < n; i++)
{
printf("Thread Id = %ld\n", omp_get_thread_num());
//Do some work here
}
}
for和parallel for命令
for指令:将一个for循环分配到多个线程中执行;
for指令可以和parallel合并使用,parallel for;
for指令也可以单独用在parallel语句的并行域中。
for结构有隐式路障
格式
#pragma omp [parallel] for [子句]
for循环语句
parallel与for分离的版本:
int main()
{
int j = 0;
#pragma omp parallel
{
#pragma omp for
for (j = 0; j < 4; j++)
{
printf("j = % d, ThreadId = % d\n", j, omp_get_thread_num());
}
}
}
parallel 将紧跟的程序块扩展为若干相同的并行区域
for 将循环中工作分配到线程组中
parallel for 版本:
#pragma omp parallel for
for(int i = 0; i < 10; i++)
{
printf("i = % d, ThreadId = % d\n", i, omp_get_thread_num());
}
外层循环并行化
int main()
{
int i, j;
omp_set_num_threads(4);
#pragma omp parallel for private(j)
for (i = 0; i < 2; i++)
{
for (j = 0; j < 6; j++)
{
printf("i = % d,j = % d, ThreadId = % d\n", i,j, omp_get_thread_num());
}
}
}
i = 0,j = 0, ThreadId = 0
i = 0,j = 1, ThreadId = 0
i = 0,j = 2, ThreadId = 0
i = 0,j = 3, ThreadId = 0
i = 0,j = 4, ThreadId = 0
i = 0,j = 5, ThreadId = 0
i = 1,j = 0, ThreadId = 1
i = 1,j = 1, ThreadId = 1
i = 1,j = 2, ThreadId = 1
i = 1,j = 3, ThreadId = 1
i = 1,j = 4, ThreadId = 1
i = 1,j = 5, ThreadId = 1
我们可以看到外部循环的线程ID一致,这就是外层循环并行化。
内部循环并行化
将指令语句移动至内循环即可
int main()
{
int i, j;
omp_set_num_threads(4);
for (i = 0; i < 2; i++)
{
#pragma omp parallel for
for (j = 0; j < 6; j++)
{
printf("i = % d,j = % d, ThreadId = % d\n", i,j, omp_get_thread_num());
}
}
}
i = 0,j = 0, ThreadId = 0
i = 0,j = 1, ThreadId = 0
i = 0,j = 4, ThreadId = 2
i = 0,j = 5, ThreadId = 3
i = 0,j = 2, ThreadId = 1
i = 0,j = 3, ThreadId = 1
i = 1,j = 0, ThreadId = 0
i = 1,j = 1, ThreadId = 0
i = 1,j = 4, ThreadId = 2
i = 1,j = 2, ThreadId = 1
i = 1,j = 3, ThreadId = 1
i = 1,j = 5, ThreadId = 3
循环并行化语句的限制:
- 并行化的语句必须是for循环语句
- 能够推测出循环的次数
- 在循环过程中不能使用break语句
- 不能使用goto和return语句从循环中跳出
- 可以使用continue语句
循环并行化问题
循环迭代
求一个数组的前若干项的值,公式如下,s[k]=s[k-2]+2*k-1
int main()
{
int i, s[1024];
s[0] = 0;
s[1] = 1;
#pragma omp parallel for schedule(static,1) num_threads(2)
//schedule(static,size)的含义
//OpenMP会给每个线程分配size次迭代计算。这个分配是静态的,
for (i = 2; i < 1024; i++)
{
s[i] = s[i - 2] + 2 * i - 1;
}
}
数据竞争问题
一个数组求和的问题,sum=sum+a[k]
- 并行区域内定义的局部变量及循环迭代变量都是私有变量;
- 并行区域内的静态变量是共享变量;
- 并行区域外定义的变量在进入并行区域是共享变量,可通过数据作用域子句消除。
int a[1000000];
//可自行定义
int main()
{
int sum = 0;
#pragma omp parallel for
for (long i = 1; i < 1000000; i++)
{
sum += a[i];
}
}
sections和section命令
sections定义一个区域,section将此区域划分成几个不同的段,各个段由一组线程并行执行。
格式:
#pragma omp [parallel] sections [子句]
{
#pragma omp section
{ 代码块 }
#pragma omp section
{ 代码块 }
…
}
int main()
{
#pragma omp parallel
{
#pragma omp sections
{
#pragma omp section
printf("section1 thread = % d\n", omp_get_thread_num());
#pragma omp section
printf("section2 thread = % d\n", omp_get_thread_num());
}
#pragma omp sections
{
#pragma omp section
printf("section3 thread = % d\n", omp_get_thread_num());
#pragma omp section
printf("section4 thread = % d\n", omp_get_thread_num());
}
}
}
子句
private (list)
firstprivate (list)
lastprivate (list)
reduction (operator: list)
nowait
sections结构有隐式路障,nowait子句会去掉这个路障。
int i;
float a[N], b[N], c[N];
for (i = 0; i < N; i++)
a[i] = b[i] = i * 1.0;
#pragma omp parallel shared(a,b,c) private(i)
{
#pragma omp sections nowait
{
#pragma omp section
for (i = 0; i < N / 2; i++)
c[i] = a[i] + b[i];
#pragma omp section
for (i = N / 2; i < N; i++)
c[i] = a[i] + b[i];
}
}
}
single命令
single指定代码块由线程组中的一个线程执行。
single结构有隐式路障:线程组中没有执行single指令的线程会一直等待代码块的结束,使用nowait子句除外。
int main()
{
omp_set_num_threads(4);
#pragma omp parallel
{
cout << "test OpenMP"<<endl;
#pragma omp single
{
cout << "test OpenMP single" << endl;
cout << "execute thread is " << omp_get_thread_num() << endl;
}
}
}
test OpenMP
test OpenMP single
execute thread is 0
test OpenMP
test OpenMP
test OpenMP
master命令
master指定代码段由主线程执行,其它线程跨越该块。master结构无隐式路障
int main()
{
int a[5], i;
#pragma omp parallel
{
#pragma omp for
for (i = 0; i < 5; i++)
a[i] = i * i;
#pragma omp master
for (i = 0; i < 5; i++)
printf_s("a[%d] = %d\n", i, a[i]);
#pragma omp barrier
#pragma omp for
for (i = 0; i < 5; i++)
a[i] += i;
}
}
三、数据处理
threadprivate命令
threadprivate指定的全局变量被各线程复制了一份私有拷贝,即各线程的私有全局变量。
语句格式:#pragma omp threadprivate (list)
各线程的私有计数器
int counter = 0;
#pragma omp threadprivate (counter)
void inc_counter()
{
counter++;
}
int main()
{
#pragma omp parallel
for (int i = 0; i < 10000; i++)
inc_counter();
printf("counter = % d\n", counter);
}
private子句
private将一个或多个变量声明成线程私有的变量,每个线程有自己变量私有副本 。
格式:private(list)
private声明的私有变量不能继承同名变量的值
int main()
{
int k = 100;
#pragma omp parallel for private(k)
for ( k=0; k < 10; k++)
{
printf("k=%d,thread = % d\n", k, omp_get_thread_num());
}
printf("last k=%d\n", k);
}
k=6,thread = 6
k=2,thread = 2
k=1,thread = 1
k=5,thread = 5
k=0,thread = 0
k=4,thread = 4
k=3,thread = 3
k=7,thread = 7
k=8,thread = 8
k=9,thread = 9
last k=100
firstprivate子句
firstprivate子句是private子句的超集
对私有变量进行初始化,把串行变量值拷贝到私有变量中(线程开始)
语句格式: firstprivate (list)
int main()
{
int k = 100;
#pragma omp parallel for firstprivate(k)
for ( int i=0; i < 4; i++)
{
k+=i;
printf("k=%d\n",k);
}
printf("last k=%d\n", k);
}
k=101
k=100
k=102
k=103
last k=100
lastprivate子句
对私有变量最后的终结操作,把私有变量(最后的循环迭代或段 )拷贝到同名串行变量中
语句格式 : lastprivate (list)
int main()
{
int val = 8;
#pragma omp parallel for firstprivate(val) lastprivate(val)
for (int i = 0; i < 2; i++) {
printf("i = % d val = % d\n", i, val);
if (i == 1)
val = 10000;
printf("i = % d val = % d\n", i, val);
}
printf("val = % d\n", val);
}
i = 1 val = 8
i = 1 val = 10000
i = 0 val = 8
i = 0 val = 8
val = 10000
share子句
shared声明一个或多个变量是共享变量
格式 shared (list)
问题:数据竞争
- 数据保护
- 尽量将共享变量转化为私有变量
default子句
default (shared | none)
default(shared):传入并行区域内的同名变量被当作共享变量来处理,不会产生私有副本。
default(none):并行区域中用到的变量必须显示指定是共享变量还是私有变量。
reduction子句
对一个或多个变量指定一个操作符,每个线程创建变量的私有副本。
在区域结束处,将用私有副本的值通过指定的操作符运算,运算结果更新原始变量。
格式 : reduction (operator: list)
初始时,每个线程都保留一份私有拷贝
在结构尾部根据指定的操作对线程中的相应变量进行规约,并更新该变量的全局值
int main()
{
int i, n, chunk;
float a[100], b[100], result;
n = 100;
chunk = 10;
result = 0.0;
for (i = 0; i < n; i++)
{
a[i] = i * 1.0;
b[i] = i * 2.0;
}
#pragma omp parallel for default(shared) private(i)\
schedule(static,chunk) reduction(+:result)
for (i = 0; i < n; i++)
result = result + (a[i] * b[i]);
printf("Final result= %f\n", result);
}
copyin子句
copyin:将主线程中threadprivate变量的值拷贝到并行区域的各个线程的threadprivate变量中。
格式 : copyin(list)
int global = 0;
#pragma omp threadprivate(global)
int main()
{
global = 1000;
#pragma omp parallel copyin(global)
{
printf("global = % d\n", global);
global = omp_get_thread_num();
}
printf("global = % d\n", global);
printf("parallel again\n");
#pragma omp parallel
printf("global = % d\n", global);
}
copyprivate子句
copyprivate:将single线程的变量值广播到同一并行区域的其他线程。
语法 : copyprivate(list)
int counter = 0;
#pragma omp threadprivate(counter)
int increment_counter()
{
counter++;
return(counter);
}
int main()
{
omp_set_num_threads(2);
#pragma omp parallel
{
int count;
#pragma omp single copyprivate(counter)
{
counter = 50;
//广播到其他线程
}
count = increment_counter();
cout << omp_get_thread_num() << " " << count<<endl;
}
return 0;
}
1 51
0 51
四、任务调度
schedule子句的格式:schedule(type[,size])
静态调度
不使用size参数
int main()
{
#pragma omp parallel for schedule(static)
for (int i = 0; i < 8; i++)
{
printf("i=%d, thread_id=%d\n", i, omp_get_thread_num());
}
}
i=0, thread_id=0
i=2, thread_id=2
i=1, thread_id=1
i=3, thread_id=3
i=6, thread_id=6
i=7, thread_id=7
i=4, thread_id=4
i=5, thread_id=5
比较散乱,每个线程分配到了迭代。
使用size参数
int main()
{
#pragma omp parallel for schedule(static,2)
for (int i = 0; i < 8; i++)
{
printf("i=%d, thread_id=%d\n", i, omp_get_thread_num());
}
}
i=0, thread_id=0
i=1, thread_id=0
i=2, thread_id=1
i=3, thread_id=1
i=6, thread_id=3
i=7, thread_id=3
i=4, thread_id=2
i=5, thread_id=2
每个线程都分到了两次迭代
动态调度
不使用size参数
int main()
{
#pragma omp parallel for schedule(dynamic)
for (int i = 0; i < 8; i++)
{
printf("i=%d, thread_id=%d\n", i, omp_get_thread_num());
}
}
i=1, thread_id=3
i=5, thread_id=3
i=6, thread_id=3
i=3, thread_id=2
i=4, thread_id=1
i=0, thread_id=0
i=2, thread_id=5
i=7, thread_id=4
每次分配一个迭代给线程
使用size参数
int main()
{
#pragma omp parallel for schedule(dynamic,2)
for (int i = 0; i < 8; i++)
{
printf("i=%d, thread_id=%d\n", i, omp_get_thread_num());
}
}
i=2, thread_id=0
i=3, thread_id=0
i=4, thread_id=2
i=5, thread_id=2
i=0, thread_id=1
i=1, thread_id=1
i=6, thread_id=11
i=7, thread_id=11
每个2个连续的迭代都分配了一个线程
guided调度
int main()
{
omp_set_num_threads(3);
//设置线程
#pragma omp parallel for schedule(guided,2)
for (int i = 0; i < 5; i++)
{
printf("i=%d, thread_id=%d\n", i, omp_get_thread_num());
}
}
i=0, thread_id=0
i=1, thread_id=0
i=2, thread_id=0
i=3, thread_id=0
i=8, thread_id=0
i=9, thread_id=0
i=6, thread_id=2
i=7, thread_id=2
i=4, thread_id=1
i=5, thread_id=1
初始分配较大的迭代次数,迭代次数按指数级下降到size次。如果没有size参数,则为1。
五、线程同步
互斥锁:用来保护一块共享存储空间,限制只有一个线程访问这块共享存储空间,保证了数据的完整性。
临界区(critical),原子操作(atomic),由库函数来提供同步操作(互斥函数)
事件同步:用来控制代码的执行顺序,使得某部分代码必须在其它代码执行完毕后才能执行。
同步路障(barrier),顺序语句(ordered)
临界区(critical)
critical:表明作用域中的代码一次只能由一个线程执行,其他线程被阻塞。
临界区代码:
#pragma omp critical [(name)]
block
int main()
{
int sum = 0;
#pragma omp parallel for
for (int i = 0; i < 10000; ++i)
{
#pragma omp critical
{
sum = sum + i;
}
}
cout << sum << endl;
}
输出是确定的49995000,如果注释掉critical 则不确定。
原子操作(atomic)
现代体系结构的多处理机提供了原子更新内存单元的方法,提供了一种更高效率的互斥锁机制。
atomic:指定特定的存储单元将被原子更新。
原子操作代码:
#pragma omp atomic
表达式;
int main()
{
int sum = 0;
#pragma omp parallel for
for (int i = 0; i < 10000; ++i)
{
#pragma omp atomic
sum += i;
}
cout << sum << endl;
}
结果任然是确定的值,避免了可能出现的数据访问竞争情况。
atomic在使用中需要注意:
- 当对一个数据进行原子操作的时候,就不能对数据进行临界区的保护
- 用户在针对同一个内存单元使用院子操作的时候,需要在程序所有涉及到该变量并行赋值的部位都加入原子操作的保护。
库函数的互斥锁支持
omp_lock_t lock;
int counter = 0;
void inc_counter()
{
printf("thread id = % d\n", omp_get_thread_num());
for (int i = 0; i < 1000; i++)
{
omp_set_lock(&lock);
counter++;
omp_unset_lock(&lock);
}
}
void dec_counter()
{
printf("thread id = % d\n", omp_get_thread_num());
for (int i = 0; i < 1000; i++)
{
omp_set_lock(&lock);
counter--;
omp_unset_lock(&lock);
}
}
int main()
{
omp_init_lock(&lock);
//初始化
#pragma omp parallel sections
{
#pragma omp section
inc_counter();
#pragma omp section
dec_counter();
}
omp_destroy_lock(&lock);
printf("counter = % d\n", counter);
}
thread id = 0
thread id = 1
counter = 0
同步路障
隐式的同步路障
- #pragma omp parallel
- #pragma omp for
- #pragma omp single
- #pragma omp sections
nowait子句:可以避免不必要的同步路障。
显示的同步路障
可以在需要的地方插入明确的同步路障语句
#pragma omp barrier
在并行区域的执行过程中,所有的执行线程都会在同步路障语句上进行同步
void initialization()
{
int counter = 0;
printf("thread %d start initialization\n", omp_get_thread_num());
for (int i = 0; i < 100000; i++)
counter++;
printf("thread %d finish initialization\n", omp_get_thread_num());
}
void process()
{
int counter = 0;
printf("thread %d start process\n", omp_get_thread_num());
for (int i = 0; i < 100000; i++)
counter++;
printf("thread %d finish process\n", omp_get_thread_num());
}
int main()
{
omp_set_num_threads(2);//2个线程
#pragma omp parallel
{
initialization();
#pragma omp barrier //路障 上一个函数等待执行完毕
process();
}
return 0;
}
thread 0 start initialization
thread 1 start initialization
thread 0 finish initialization
thread 1 finish initialization
thread 1 start process
thread 0 start process
thread 1 finish process
thread 0 finish process
顺序语句(ordered)
ordered:指定循环区域内的代码段按循环迭代的顺序执行。
ordered命令需要和ordered子句结合起来使用。
格式:#pragma omp ordered
int main()
{
#pragma omp parallel for ordered schedule(static, 2)
for (int i = 0; i < 10; i++)
{
#pragma omp ordered
//按照循环顺序来,不会出现顺序混乱
printf("i = % d\n", i);
}
return 0;
}
flush指导语句
flush指导语句用以标识一个同步点,用以确保所有的线程看到一致的存储器视图
语句格式 #pragma omp flush (list) newline
flush将在下面几种情形下隐含运行,nowait子句除外
Barrier ,critical:进入与退出部分
ordered:进入与退出部分 ,parallel:退出部分
for:退出部分 , sections:退出部分,single:退出部分
六、常用库函数
设置线程数量
#include <omp.h>
#ifdef _OPENMP
omp_set_num_threads(4);
//设置4个线程
#endif
获取线程ID号
返回当前线程号,0代表主线程
int omp_get_thread_num(void)
环境变量
七、例子
计算PI的值
static long num_steps = 1000000;
double step;
#define NUM_THREADS 2
void main()
{
double x, pi = 0.0, sum[NUM_THREADS];
step = 1.0 / (double)num_steps;
omp_set_num_threads(NUM_THREADS);
#pragma omp parallel private(x)
//每个线程自己私有副本
{
int id = omp_get_thread_num();
sum[id] = 0;
#pragma omp for
//以下for循环分给各个线程执行,结果保存在sum[id]里
for (int i = 0; i < num_steps; i++)
{
x = i * step;
sum[id] += 4.0 / (1.0 + x * x);
}
}
//各个线程的结果相加
for (int i = 0; i < NUM_THREADS; i++)
pi += sum[i] * step;
cout << pi << endl;
}