OpenMP 指令的通用格式为
#pragma omp 指令 [子句[子句]...]
OpenMP 并行执行的程序要全部结束后才会运行后面的非并行部分的代码, 这就是fork/join并行模式.
parallel 是构造并行块的一个指令, 在这个指令后面需要使用一对大括号来指定需要并行计算的代码。
#pragma omp parallel []
{
}
for指令的作用是使一个for循环在多个线程中执行,一般for指令会与parallel指令同时使用,即parallel for指令。
#include
#include
int main(int argc, char* argv[])
{
#pragma omp parallel
{
int i;
#pragma omp for
for (i = 0; i < 5; i++)
printf("i = %d\n", i);
}
return 0;
}
/* OUTPUT */
/*
i = 3
i = 4
i = 1
i = 0
i = 2
*/
#pragma omp for
块内语句是并行执行的, 可以从输出中看出, 输出的 i 的值并不是顺序的.
sections 可以把代码分块, 通过 section 指令分出的块, 每一块都会并行执行.
int main(int argc, char* argv[])
{
#pragma omp parallel sections
{
#pragma omp section
printf("Section 1 ThreadId = %d\n", omp_get_thread_num());
#pragma omp section
printf("Section 2 ThreadId = %d\n", omp_get_thread_num());
#pragma omp section
printf("Section 3 ThreadId = %d\n", omp_get_thread_num());
#pragma omp section
printf("Section 4 ThreadId = %d\n", omp_get_thread_num());
}
return 0;
}
/* OUTPUT */
/*
Section 1 ThreadId = 30
Section 2 ThreadId = 16
Section 4 ThreadId = 0
Section 3 ThreadId = 21
*/
输出结果中的 Section 并不是顺序的, 可见每一个 section 是并行执行的.
private 子句可以将变量声明为线程私有,声明称线程私有变量以后,每个线程都有一个该变量的副本,线程之间不会互相影响,其他线程无法访问其他线程的副本。原变量在并行部分不起任何作用,也不会受到并行部分内部操作的影响。
int main(int argc, char* argv[])
{
int i = 20;
#pragma omp parallel for private(i)
for (i = 0; i < 10; i++)
{
printf("i = %d\n", i);
}
printf("outside i = %d\n", i);
return 0;
}
/* OUTPUT */
/*
i = 2
i = 1
i = 4
i = 7
i = 3
i = 8
i = 0
i = 9
i = 5
i = 6
outside i = 20
*/
private子句不能继承原变量的值,但是有时我们需要线程私有变量继承原来变量的值,这样我们就可以使用firstprivate子句来实现。
int main(int argc, char* argv[])
{
int t = 20, i;
#pragma omp parallel for firstprivate(t)
for (i = 0; i < 5; i++)
{
t += i;
printf("t = %d\n", t);
}
printf("outside t = %d\n", t);
return 0;
}
/* OUTPUT */
/*
t = 20
t = 22
t = 24
t = 23
t = 21
outside t = 20
*/
这个例子中的 t 继承了并行代码块外的 t 的值. 对于每个并行的进程来说, t = 20 .
并行代码块结束后, t 的值仍为 20.
有时候我们需要在退出并行部分后把计算的结果返回到原来变量.
int main(int argc, char* argv[])
{
int t = 20, i;
#pragma omp parallel for firstprivate(t), lastprivate(t)
for (i = 0; i < 5; i++)
{
t += i;
printf("t = %d\n", t);
}
printf("outside t = %d\n", t);
return 0;
}
/* OUTPUT1 */
/*
t = 24
t = 21
t = 23
t = 20
t = 22
outside t = 24
*/
/* OUTPUT2 */
/*
t = 20
t = 21
t = 22
t = 23
t = 24
outside t = 24
*/
从上面的例子中, 可能会有这样的问题: 如果是并行的话, 为什么返回的值多次运行一直都是 24?
根据OpenMP规范,在循环迭代中,是最后一次迭代的值赋值给原变量;如果是section结构,那么是程序语法上的最后一个section语句赋值给原变量。
threadprivate子句可以将一个变量复制一个私有的拷贝给各个线程,即各个线程具有各自私有的全局对象。
#pragma omp threadprivate(list)
将一个变量声明为共享变量, 在多个线程内共享.
需要注意的是,在并行部分进行写操作时,要求共享变量进行保护,否则不要随便使用共享变量,尽量将共享变量转换为私有变量使用。
int main(int argc, char* argv[])
{
int t = 20, i;
#pragma omp parallel for shared(t)
for (i = 0; i < 10; i++)
{
if (i % 2 == 0)
t++;
printf("i = %d, t = %d\n", i, t);
}
return 0;
}
/* OUTPUT */
/*
i = 4, t = 21
i = 0, t = 24
i = 5, t = 24
i = 9, t = 24
i = 6, t = 25
i = 1, t = 20
i = 2, t = 22
i = 3, t = 22
i = 7, t = 22
i = 8, t = 23
*/
reduction子句可以对一个或者多个参数指定一个操作符,然后每一个线程都会创建这个参数的私有拷贝,在并行区域结束后,迭代运行指定的运算符,并更新原参数的值。
私有拷贝变量的初始值依赖于redtution的运算类型。
int main(int argc, char* argv[])
{
int i, sum = 10;
#pragma omp parallel for reduction(+: sum)
for (i = 0; i < 10; i++)
{
sum += i;
printf("%d\n", sum);
}
printf("sum = %ld\n", sum);
return 0;
}
/* OUTPUT */
/*
0
9
1
3
4
5
2
7
6
8
sum = 55
*/
copyin子句可以将主线程中变量的值拷贝到各个线程的私有变量中,让各个线程可以访问主线程中的变量。
copyin的参数必须要被声明称threadprivate,对于类的话则并且带有明确的拷贝赋值操作符。
int g = 0;
#pragma omp threadprivate(g)
int main(int argc, char* argv[])
{
int i;
#pragma omp parallel for
for (i = 0; i < 4; i++)
{
g = omp_get_thread_num();
printf("thread %d, g = %d\n", omp_get_thread_num(), g);
}
printf("global g: %d\n", g);
#pragma omp parallel for copyin(g)
for (i = 0; i < 4; i++)
printf("thread %d, g = %d\n", omp_get_thread_num(), g);
return 0;
}
/* OUTPUT */
/*
thread 0, g = 0
thread 1, g = 1
thread 2, g = 2
thread 3, g = 3
global g: 0
thread 0, g = 0
thread 1, g = 0
thread 2, g = 0
thread 3, g = 0
*/
在上面的例子中, 并行代码的第一块中, g 是每个线程的私有变量. 在第二块并行代码中, g 是从主线程中拷贝得到的值, 所以 g 在多个线程输出0.
当parallel for没有带schedule时,大部分情况下系统都会默认采用static调度方式。假设有n次循环迭代,t个线程,那么每个线程大约分到n/t次迭代。这种调度方式会将循环迭代均匀的分布给各个线程,各个线程迭代次数可能相差1次。用法为schedule(method)。
int main(int argc, char* argv[])
{
int i;
#pragma omp parallel for schedule(static)
for (i = 0; i < 10; i++)
{
printf("i = %d, thread %d\n", i, omp_get_thread_num());
}
return 0;
}
/* OUTPUT */
/*
i = 2, thread 2
i = 8, thread 8
i = 4, thread 4
i = 6, thread 6
i = 0, thread 0
i = 1, thread 1
i = 9, thread 9
i = 7, thread 7
i = 3, thread 3
i = 5, thread 5
*/
在静态调度的时候,我们可以通过指定size参数来分配一个线程的最小迭代次数。指定size之后,每个线程最多可能相差size次迭代。可以推断出[0,size-1]的迭代是在第一个线程上运行,依次类推。
int main(int argc, char* argv[])
{
int i;
// YOUR CODE HERE
#pragma omp parallel for schedule(static, 1)
// END OF YOUR CODE
for (i = 0; i < 10; i++)
{
printf("i = %d, thread %d\n", i, omp_get_thread_num());
}
return 0;
}
/* OUTPUT */
/*
size = 1;
i = 0, thread 0
i = 3, thread 3
i = 7, thread 7
i = 9, thread 9
i = 6, thread 6
i = 5, thread 5
i = 4, thread 4
i = 1, thread 1
i = 2, thread 2
i = 8, thread 8
size = 3;
i = 0, thread 0
i = 1, thread 0
i = 2, thread 0
i = 9, thread 3
i = 3, thread 1
i = 4, thread 1
i = 5, thread 1
i = 6, thread 2
i = 7, thread 2
i = 8, thread 2
*/
动态分配是将迭代动态分配到各个线程,依赖于运行你状态来确定,所以我们无法像静态调度一样事先预计进程的分配。哪一个线程先启动,哪一个线程迭代多久,这些都取决于系统的资源和线程的调度。
int main(int argc, char* argv[])
{
int i;
#pragma omp parallel for schedule(dynamic)
for (i = 0; i < 10; i++)
{
printf("i = %d, thread %d\n", i, omp_get_thread_num());
}
return 0;
}
/* OUTPUT */
/*
i = 0, thread 25
i = 4, thread 5
i = 2, thread 31
i = 3, thread 18
i = 6, thread 26
i = 1, thread 27
i = 5, thread 0
i = 7, thread 19
i = 9, thread 20
i = 8, thread 29
*/
获取执行函数时可以使用的处理器的数目.
返回当前并行区域中的活动线程的个数, 如果在并行区域外调用返回 1
int main(int argc, char* argv[])
{
printf("%d\n", omp_get_num_threads());
#pragma omp parallel
{
printf("%d\n", omp_get_num_threads());
}
return 0;
}
/* OUTPUT */
/*
1
32
32
32
32
32
32
32
32
32
32
32
32
32
32
32
32
32
32
32
32
32
32
32
32
32
32
32
32
32
32
32
32
*/
返回当前线程的线程号.
设置进入并行区域时, 将要创建的线程的个数.
判断当前是否处于并行状态.
返回 0 表示不处于并行状态, 返回 1 表示处于并行状态.
获取最大线程数量. 根据OpenMP文档中的规定,这个最大数量是指在不使用num_threads的情况下,OpenMP可以创建的最大线程数量。需要注意的是这个值是确定的,与它是否在并行区域调用没有关系。
void omp_init_lock(omp_lock); //初始化互斥锁
void omp_destroy_lock(omp_lock); //销毁互斥锁
void omp_set_lock(omp_lock); //获取互斥锁
void omp_unset_lock(omp_lock); //释放互斥锁
static omp_lock_t lock;
int main(int argc, char* argv[])
{
int i;
omp_init_lock(&lock);
#pragma omp parallel for
for (i = 0; i < 5; ++i)
{
omp_set_lock(&lock);
printf("%d+\n", omp_get_thread_num());
printf("%d-\n", omp_get_thread_num());
omp_unset_lock(&lock);
}
omp_destroy_lock(&lock);
return 0;
}
/* OUTPUT */
/*
0+
0-
3+
3-
1+
1-
4+
4-
2+
2-
*/
以上例子中, 虽然线程完成的顺序不一定, 但是一定是+
和-
匹配的,原因是互斥锁锁上后,其他进程必须等待获取到互斥锁的进程释放互斥锁.
倘若我们把互斥锁删除:
int main(int argc, char* argv[])
{
int i;
#pragma omp parallel for
for (i = 0; i < 5; ++i)
{
printf("%d+\n", omp_get_thread_num());
printf("%d-\n", omp_get_thread_num());
}
return 0;
}
/* OUTPUT */
/*
1+
0+
0-
1-
4+
4-
3+
3-
2+
2-
*/
就有可能出现不匹配的情况.
用于 尝试获得锁, 这个函数可以看作是omp_set_lock
的非阻塞版本.
static omp_lock_t lock;
int main(int argc, char* argv[])
{
int i;
omp_init_lock(&lock);
#pragma omp parallel for
for (i = 0; i < 5; ++i)
{
if (omp_test_lock(&lock))
{
printf("%d+\n", omp_get_thread_num());
printf("%d-\n", omp_get_thread_num());
omp_unset_lock(&lock);
}
else
{
printf("fail to get lock\n");
}
}
omp_destroy_lock(&lock);
return 0;
}
/* OUTPUT */
/*
fail to get lock
fail to get lock
3+
3-
fail to get lock
1+
1-
*/
非阻塞: 对比上一个程序, 这种方法一旦没有机会获取到锁, 就不接着与其他进程竞争, 而是进行接下来的代码.
阻塞: 上一个程序, 如果没有获取到锁, 会接着等待自己获取到锁. 所以上个程序每一个线程都有输出.
s设置是否允许在运行时动态调整并行区域的线程数.
void omp_set_dynamic(int)
当传入参数为 0 时动态调整禁用.
当传入参数非 0 时,系统自动调整以最佳利用系统资源.
该函数可以返回当前程序是否允许在运行时动态调整并行区域的线程数。
int omp_get_dynamic(void)
当返回值为非0时表示允许系统动态调整线程。
当返回值为0时表示不允许。