openmp并行程序设计时,经常会发生线程之间的竞争,即可能同时访问同一共享变量。当然如果共享变量在线程并发期间不改变值的话,同时读取共享变量不会产生任何问题,除非同时写该变量,则无法确定该线程间该变量的写顺序。因此需要设置隔离区,线程之间只能顺序去执行该隔离区的命令。
(1) 临界区(critical)
(2) 原子操作(atomic)
(3) 库函数来提供同步操作
![]()
openmp中有一些同步机制可以避免线程竞争问题的发生。可以使用同步机制,使得线程只有执行到某个语句,才能继续执行后面的程序。在程序需要访问可能产生竞争的内存数据的时候,都需要插入相应的临界区代码。
一个例子是定积分确定求圆周率Pi的值:
函数接口只需要确定区间0-1之间的分段数,段数越多,PI的值自然越精确。
串行程序非常简单:
double get_pi(const int& N)
{
assert(N > 0);
double pi = 0;
int i;
double x;
double sigma = 1.0/N;
for (i = 0; i < N; ++i)
{
x = sigma*(i + 0.5);
pi += sigma*4.0/(1.0 + x*x);
}
return pi;
}
而使用openmp的并行程序中,求取不同段的段值之间并没有数据相关性,因此存在潜在的并行性。唯一的问题是如何定义每段的段值变量,是用一个长度为段数的数组,还是只用一个变量定义即可呢?前者的并行思路非常清晰,但是如果段数很多,无疑需要很大的空间;后者当然是我们所追求的,既能加速又能避免不必要的空间开销。但如果不同线程计算段值使用同一变量,如何避免线程之间的竞争呢?
很明显,只要隔离开不同线程之间的操作顺序即可。通过临界区,使得在整体求和过程中只有一个线程能够同时执行求和部分代码;或者使用互斥锁函数或原子操作等,不同的线程执行各自的段值私有变量的求和,线程之间也是隔离开来。
在OpenMP中,提供了三种不同的互斥锁机制用来对一块内存进行保护,它们分别是:
(1) 临界区(critical)
(2) 原子操作(atomic)
(3) 库函数来提供同步操作
double get_pi_omp(const int& N)
{
assert(N > 0);
double pi = 0;
double sigma = 1.0/N;
int i;
double x;
double sum = 0;
//#pragma omp parallel private(x) firstprivate(sum) num_threads(4)
// {
// #pragma omp for
// for (i = 0; i < N; ++i)
// {
// x = sigma*(i + 0.5);
// sum += sigma*4/(x*x + 1);
// //printf("threadid = %d, sum = %f\n", omp_get_thread_num(), sum);
// }
// #pragma omp critical
// {
// pi += sum;
// }
// }
omp_lock_t mylock;
omp_init_lock(&mylock);
#pragma omp parallel private(x) firstprivate(sum) num_threads(4)
{
#pragma omp for
for (i = 0; i < N; ++i)
{
x = sigma*(i + 0.5);
sum += sigma*4/(x*x + 1);
//printf("threadID = %d, sum = %f\n", omp_get_thread_num(), sum);
}
omp_set_lock(&mylock);
pi += sum;
omp_unset_lock(&mylock);
}
omp_destroy_lock(&mylock);
//#pragma omp parallel private(x) firstprivate(sum) num_threads(4)
// {
// #pragma omp for
// for (i = 0; i < N; ++i)
// {
// x = sigma*(i + 0.5);
// sum += sigma*4/(x*x + 1);
// //printf("threadID = %d, sum = %f\n", omp_get_thread_num(), sum);
// }
// #pragma omp atomic
// pi += sum;
// }
return pi;
}
在main函数中检测,设置0-1之间的分段数n = 120000000, 检测代码以及结果如图:
int main(int argc, char** argv)
{
int n;
puts("Input n\n");
scanf("%d", &n);
//get pi value and calcute cost sequential time
clock_t first = clock();
double pi = get_pi(n);
clock_t last = clock();
printf("pi = %f \n", pi);
printf("The sequential cost time is %f seconds\n", (double)(last - first)/CLOCKS_PER_SEC);
//get pi value and calcute cost parallel time
first = clock();
pi = get_pi_omp(n);
last = clock();
printf("--------------------------------------------------\n");
printf("pi = %f\n", pi);
printf("The parallel cost time is %f seconds\n", (double)(last - first)/CLOCKS_PER_SEC);
getchar();
return 0;
}
在
双核四线程i3 CPU处理器下,开启四个线程,三种互斥锁的方式效率是极其相近的,
都在0.53s左右,而串行执行为1.08s。