[转]OpenMP与C++:事半功倍地获得多线程的好处(下)
用以同步的编译器指令
在多个线程并发的时候,某一线程常常会需要同步其它线程。OpenMP支持多种类型的同步,以在不同的情境下解决问题。
其中之一就是暗含的barrier同步。在每一个并行区域都有一个暗含的barrier,用以同步并行区域中的所有线程。一个barrier同步要求所有线程执行到此,然后才能往下执行。
#pragma omp for,#pragma omp single和#pragma omp sections程序块都有暗含的barrier同步。从上述三种工作共享的程序块中去除暗含的barrier同步的方法是增加nowait子句:
#pragma omp parallel
{
#pragma omp for nowait
for(int i = 1; i < size; ++i)
x[i] = (y[i-1] + y[i+1])/2;
}
如你所见,工作共享指令中的nowait子句指明线程不需要在for循环结束时同步,尽管线程将在并行区域结束处同步。
另一种是明确声明barrier同步,在一些情境下你可能需要在并行区域出口之外放置barrier同步,这时你可以在代码里加一个#pragma omp barrier指令。
临界区能够像barrier那样使用,在Win32 API中通过EnterCriticalSection和ExitCriticleSection来进出临界区。OpenMP通过#pragma omp critical [name]指令给予程序员同样的能力。这与Win32临界区有同样的语义,并且隐藏了EnterCriticleSection的调用。你可以使用命名的临界区,这种情况下代码段仅与同名临界区互斥。如果没有指定临界区名字,则映射到用户未定义的名字。这些未命名的临界区与区域相关的每一临界区互斥。
在一个并行区域里,经常限制同时只有一条线程能够访问一段代码,例如在并行区域的中间写文件。大多数这种情况下,并不关心哪一条线程执行这段代码,只要只有一条线程执行这段代码即可,OpenMP用#pragma omp single指令来完成这个工作。
有此时候用single指令声明必须由单一线程执行并行区域中的一段代码并不满足需要。有些情况下你希望确保主线程来执行这段代码——例如主线程是GUI线程并且你希望GUI线程完成一些工作。#pragma omp master指令可以做到这一点。不像single,在进出一个master代码块的时候并没有暗含的barrier。
内存界定(Memory Fence)可用#pragma omp flush实现,这条指令在程序中生成内存界定,它的本质上等效于_ReadWriteBarrier。
切记OpenMP指令同时影响线程组里的所有线程。因此下面的代码片段是非法的并且有未定义的运行时行为(崩溃或者在特别情况下被挂起):
#pragma omp parallel
{
if(omp_get_thread_num() > 3)
{
#pragma omp single // May not be accessed by all threads
x++;
}
}
执行环境例程
除了前文讨论的编译器指令OpenMP也包含一系列极为有用的运行时例程,用以编写OpenMP应用程序。有三大类型的例程可用:执行环境例程,锁/同步例程和定时例程(定时例程不在本文讨论)。所有的OpenMP例程都在omp.h头文件中定义并皆以omp_开头。
运行时环境例程提供允许你查询和设置OpenMP环境的各个方面的功能。以omp_set_开头的函数只能在并行区域外调用,其它函数可在并行和非并行区域使用。
可以用omp_get_num_threads和omp_set_num_threads来读取或者设置线程组的线程数量。omp_get_num_threads返回当前线程组的线程数目。如果调用此函数的线程不在并行区域,返回1。omp_set_num_threads用以设置当前线程执行下一个并行区域的线程数。
但这并非设置线程数目的全部,并行区域的线程数目同样依赖于OpenMP的另两方面的配置环境:动态线程和嵌套。
动态线程是一个默认为不使能的布尔属性。当线程将执行一块并行区域的时候如果这个属性为不使能,那么OpenMP就生线程数量为omp_get_max_threads返回值的线程组。omp_get_max_threads默认为计算机的硬件线程数或者环境变量OMP_NUM_THREADS的值。如果使能动态线程OpenMP将生成一个线程数量可变的线程组,但这个数量不会超过omp_get_max_threads的返回值。
嵌套是另一个默认为不使能的布尔属性。并行区域嵌套出现在当线程已经运行在并行区域又遇到另一个并行区域的时候。如果嵌套被使能,那么就按前文关于动态线程的规则生成一个新的线程组。相反地,线程组就只有单独一个线程。
可以通过omp_set_dynamic、omp_get_dynamic、omp_set_nested和 omp_get_nested来设置或者查询动态线程和嵌套的使能状态。每一条线程都可以查询它所处的环境。线程可以通过调用omp_get_thread_num来获得它所处的线程组的线程数目——一个比调用omp_get_num_threads的返回值少0或者1的值。
omp_in_parallel用以查询本线程是否正在并行区域执行。omp_get_num_proc用以获知计算机有多少个CPU。
#include <stdio.h>
#include <omp.h>
int main()
{
omp_set_dynamic(1);
omp_set_num_threads(10);
#pragma omp parallel // parallel region 1
{
#pragma omp single
printf("Num threads in dynamic region is = %d\n",
omp_get_num_threads());
}
printf("\n");
omp_set_dynamic(0);
omp_set_num_threads(10);
#pragma omp parallel // parallel region 2
{
#pragma omp single
printf("Num threads in non-dynamic region is = %d\n",
omp_get_num_threads());
}
printf("\n");
omp_set_dynamic(1);
omp_set_num_threads(10);
#pragma omp parallel // parallel region 3
{
#pragma omp parallel
{
#pragma omp single
printf("Num threads in nesting disabled region is = %d\n",
omp_get_num_threads());
}
}
printf("\n");
omp_set_nested(1);
#pragma omp parallel // parallel region 4
{
#pragma omp parallel
{
#pragma omp single
printf("Num threads in nested region is = %d\n",
omp_get_num_threads());
}
}
}
(图6)使用OpenMP例程
图6可以帮助你更清晰地理解这些不同的互相作用的环境例程。在这个例子中有4个截然不同的并行区域,包括两个嵌套并行区域。
用Visual Studio 2005编译之后在双处理器的计算机上执行上例,输出如下:
Num threads in dynamic region is = 2
Num threads in non-dynamic region is = 10
Num threads in nesting disabled region is = 1
Num threads in nesting disabled region is = 1
Num threads in nested region is = 2
Num threads in nested region is = 2
在第一个并行区域使能了动态线程并设置线程数为10。从程序的输出可以看到使能了动态线程的OpenMP在运行时仅为线程组分派两条线程——因为计算机只有两个处理器。在第二个并行区域,未使能动态线程的OpenMP为线程组分派了10条线程。
在第三、四个并行区域,你可以看到使能和未使能嵌套的影响。在第三个并行区域,因为没有使能嵌套,所以没有为嵌套的并行区域分派新的线程。因此嵌套和外部并行区域加起来只有两条线程。在使能了嵌套的第四个并行区域中为嵌套并行区域生成了一个拥有两条线程的线程组(故在嵌套并行区域总计有四条线程)。这种为每一个嵌套并行区域加倍增加线程的处理能够一直进行下去,直到用完栈空间。实际上你可以生成几百条线程,但这样做的话开销将会远大于使用多线程获得的性能优势。
可能你已经留意到在第三、四并行区域中是使能了动态线程的,那么下面这段未使能动态线程的代码又会有什么样的执行结果?
omp_set_dynamic(0);
omp_set_nested(1);
omp_set_num_threads(10);
#pragma omp parallel
{
#pragma omp parallel
{
#pragma omp single
printf("Num threads in nested region is = %d\n",
omp_get_num_threads());
}
}
下面你可以看到预期的结果。在第一个并行区域开始处由一个10个线程的线程组执行,后来并发的嵌套并行区域则为10个线程中的每一个线程分派有10个线程的线程组来执行内部并行区域。因此在嵌套并行区域内部总计有100条线程执行。
Num threads in nested region is = 10
Num threads in nested region is = 10
Num threads in nested region is = 10
Num threads in nested region is = 10
Num threads in nested region is = 10
Num threads in nested region is = 10
Num threads in nested region is = 10
Num threads in nested region is = 10
Num threads in nested region is = 10
Num threads in nested region is = 10