OPENMP学习笔记(2)—— Race Condition

OPENMP学习笔记(2)—— Race Condition

Race Condition(竞争条件)

计算机运行过程中,并发、无序、大量的进程在使用有限、独占、不可抢占的资源,由于进程无限,资源有限,产生矛盾,这种矛盾称为竞争(Race)。

由于两个或者多个进程竞争使用不能被同时访问的资源,使得这些进程有可能因为时间上推进的先后原因而出现问题,这叫做竞争条件(Race Condition)。

竞争条件分为两类:
-Mutex(互斥):两个或多个进程彼此之间没有内在的制约关系,但是由于要抢占使用某个临界资源(不能被多个进程同时使用的资源,如打印机,变量)而产生制约关系。
-Synchronization(同步):两个或多个进程彼此之间存在内在的制约关系(前一个进程执行完,其他的进程才能执行),如严格轮转法。

解决互斥方法:
Busy Waiting(忙等待):等着但是不停的检查测试,不睡觉,知道能进行为止
Sleep and Wakeup(睡眠与唤醒):引入Semapgore(信号量,包含整数和等待队列,为进程睡觉而设置),唤醒由其他进程引发。

临界区(Critical Region):一段访问临界资源的代码。

竞态条件(race condition)的问题,是所有多线程编程最棘手的问题。该问题可表述为:当多个线程并行执行时,有可能多个线程同时对某变量进行了读写操作,从而导致不可预知的结果。
比如下面的例子,对于包含10个整形元素的数组a,我们用for循环求它各个元素之和,并将结果保存在变量sum里。
加入并行代码

#include<omp.h>  
#include<iostream>  
//using namespace std;

int main()
{
	int sum = 0;
	int a[10] = { 1,2,3,4,5,6,7,8,9,10 };
#pragma omp parallel for
	for (int i = 0; i < 10; i++)
		sum = sum + a[i];
	std::cout << "sum: " << sum << std::endl;
	getchar();
	return 0;
}

在这里插入图片描述
正确结果是55,但是错误的原因是,当某线程A执行sum = sum + a[i]的同时,另一线程B正好在更新sum,而此时A还在用旧的sum做累加,于是出现了错误。

基本方案:

用openMP怎么实现并行数组求和呢?先给出一个基本的解决方案。思想是,首先生成一个数组sumArray,其长度为并行执行的线程的个数(默认情况下,该个数等于CPU的核数),在for循环里,让各个线程更新自己线程对应的sumArray里的元素累加到sum里,代码如下

#include <iostream>
#include <omp.h>

int main() {
	int sum = 0;
	int a[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int coreNum = omp_get_num_procs();//获得处理器个数
	int* sumArray = new int[coreNum];//对应处理器个数,先生成一个数组
	for (int i = 0; i < coreNum; i++)//将数组各元素初始化为0
		sumArray[i] = 0;
#pragma omp parallel for
	for (int i = 0; i < 10; i++)
	{
		int k = omp_get_thread_num();//获得每个线程的ID
		sumArray[k] = sumArray[k] + a[i];
	}
	for (int i = 0; i < coreNum; i++)
		sum = sum + sumArray[i];
	std::cout << "sum: " << sum << std::endl;
	getchar();
	return 0;
}

在上面代码里,我们用omp_get_num_procs()函数来获取处理器个数,用omp_get_thread_num()函数来获得每个线程的ID,为了使用这两个函数,我们需要include<omp.h>

reduction:

上面的代码虽然达到了目的,但是它产生了较多的额外操作,比如要先生成数组sumArray,最后还要用一个for循环将它的各元素累加起来,有没有更简便的方法呢?答案是有,OpenMP为我们提供了另一个工具,归约(reduction),见下面代码

#include <iostream>
#include <omp.h>

int main(){
    int sum = 0;
    int a[10] = {1,2,3,4,5,6,7,8,9,10};
#pragma omp parallel for reduction(+:sum)
    for (int i=0;i<10;i++)
        sum = sum + a[i];
    std::cout<<"sum: "<<sum<<std::endl;
    getchar();
    return 0;
}

归约:归约操作符是序列中的两两元素做的运算,一定是一个二元运算符。归约变量则保存归约操作的中间结果。OpenMP用归约变量为每个线程创建一个私有的变量,用来存储自己归约的结果

上面代码里,我们在#pragma omp parallel for后面加上了reduction(+:sum),它的意思是告诉编译器:下面的for循环你要分成多个线程跑,但每个线程都要保存变量sum的拷贝,循环结束后,所有线程把自己的sum累加起来作为最后的输出。

reduction子句为变量指定一个操作符,每个线程都会创建reduction变量的私有拷贝,在OpenMP区域结束处,将使用各个线程的私有拷贝的值通过制定的操作符进行迭代运算,并赋值给原来的变量。
reduction的语法为recutioin(operator:list)和其他的数据属性子句不一样的是多了一个operator参数。由于最后会进行迭代运算,所以不是所有的运算符都能作为reduction的参数,而且,迭代运算需要一个初始值,不是所有的操作符需要有相同的初始值,一般而言,常见的reduction操作符的初始值为:+(0),*(1),-(0),&~(0),|(0),^(0),&&(1),||(0),当然,这不是必须的,比如叠加运算的初始值,可以是任意值,只是表达的含义不一样而已。

理解reduction的工作过程:
(1)进入并行区域后,team内的每个新的线程都会对reduction变量构造一个副本,比如,假设有四个线程,那么,进入并行区域的初始化值分别为:sum0=100,sum1 = sum2 = sum3 = 0.为何sum0为100呢?因为主线程不是一个新的线程,所以不需要再为主线程构造一个副本(没有找到官方这样的说法,但是从理解上,应该就是这样工作的,只会有一个线程使用到并行区域外的初始值,其余的都是0)。
(2)每个线程使用自己的副本变量完成计算。
(3)在退出并行区域时,对所有的线程的副本变量使用指定的操作符进行迭代操作,对于上面的例子,即sum’ = sum0’+sum1’+sum2’+sum3’.
(4)将迭代的结果赋值给原来的变量(sum),sum=sum’.
(5)主线程也会创建一个副本变量,其初始值也为0,在最后迭代的时候,是用sum原来的值和每个线程的副本进行计算。

(1)sum=100
(2)进入并行区域,创建4个线程的4个副本:sum0=sum1=sum2=sum3=0;
(3)计算完成后,得到sum0',sum1',sum2',sum3'
(4)计算sum,sum=sum op sum 0‘ op sum1’ op sum2‘ op sum3

注意:
reduction只能用于标量类型(int、float等等);
reduction只用于一个区域构造或者工作共享构造的结构中,并且,在这个区域中,reduction的变量只能被用于类似如下的语句

x = x op expr 
x = expr op x (except subtraction) 
x binop = expr 
x++ 
++x 
x-- 
--x

在并行块中对归约变量(实际上是对私有变量)做这些操作往往使用的是归约本身的操作符,如果使用其它操作符,只要满足上面的条件就是允许的,不过最后各个线程得到的结果仍然会按reduction子句指定的归约操作符所应当的归约方式(如+和-即做加法,*即做乘法)进行归约,而不会真的按照在并行块中表面上对归约变量的操作去执行:

#include<stdio.h>
#include<stdlib.h>
#include<omp.h>

int main(int argc,char *argv[])
{
    int sum=-2;
    int thrdCnt=strtol(argv[1],NULL,10);
    //归约子句(归约操作符:归约变量)
#pragma omp parallel num_threads(thrdCnt) reduction(*:sum)
    {
        int myRank=omp_get_thread_num();
        if(myRank!=0)
            sum*=myRank;
        sum++;//实际是加在私有变量上!
        printf("%d->%d\n",myRank,sum);
    }
    printf("sum=%d\n",sum);//归约结果
    return 0;
}

结果:

3->4
1->2
2->3
0->2
sum=-96

这个-96来自(13+1)(11+1)(12+1)(1+1)*(-2),总之理解好OpenMP归约的本质是在并行块里按归约操作符来生成指定初始值的私有变量,在并行块中看似对归约变量的操作是对私有变量的操作,最终再将各个线程计算好的私有变量按归约操作符的形式以特定的方式归约。

特别注意最后的归约时’-‘操作符做加操作!下面这个例子能再次证明这一点:

#include<stdio.h>
#include<stdlib.h>
#include<omp.h>

int main(int argc,char *argv[])
{
    int sum=30;
    int thrdCnt=strtol(argv[1],NULL,10);
    //归约子句(归约操作符:归约变量)
#pragma omp parallel num_threads(thrdCnt) reduction(-:sum)
    {
        int myRank=omp_get_thread_num();
        sum+=2;
        printf("%d->%d\n",myRank,sum);
    }
    printf("sum=%d\n",sum);//归约结果
    return 0;
}

结果:

3->2
1->2
2->2
0->2
sum=38

critical:

reduction虽然很方便,但它只支持一些基本操作,比如+,-,*,&,|,&&,||等。有些情况下,我们既要避免race condition,但涉及到的操作又超出了reduction的能力范围,那应该怎么办呢?这就要用到openMP的另一个工具,critical。来看下面的例子,该例中我们求数组a的最大值,将结果保存在max里。

#include <iostream>
int main(){
    int max = 0;
    int a[10] = {11,2,33,49,113,20,321,250,689,16};
#pragma omp parallel for
    for (int i=0;i<10;i++)
    {
        int temp = a[i];
#pragma omp critical
        {
            if (temp > max)
                max = temp;
        }
    }
    std::cout<<"max: "<<max<<std::endl;
    return 0;
}

上例中,for循环还是被自动分成N份来并行执行,但我们用#pragma omp critical将if(temp > max) max = temp 括了起来,它的意思是:各个线程还是并行执行for里面的语句,但当你们执行到critical里面时,要注意有没有其他线程正在里面执行,如果有的话,要等其他线程执行完再进去执行。这样就避免了race condition问题,但显而易见,它的执行速度会变低,因为可能存在线程等待情况。

&与&&

&运算符有两种用法:(1)按位与;(2)逻辑与。
&&运算符是短路与运算。
逻辑与跟短路与的差别是非常巨大的,虽然二者都要求运算符左右两端的布尔值都是true整个表达式的值才是true。
&&之所以称为短路运算是因为,如果&&左边的表达式的值是false,右边的表达式会被直接短路掉,不会进行运算。
很多时候我们可能都需要用&&而不是&,例如在验证用户登录时判定用户名不是null而且不是空字符串,应当写为:username != null &&!username.equals(“”),二者的顺序不能交换,更不能用&运算符,因为第一个条件如果不成立,根本不能进行字符串的equals比较,否则会产生NullPointerException异常。
注意:逻辑或运算符(|)和短路或运算符(||)的差别也是如此。

发布了32 篇原创文章 · 获赞 13 · 访问量 8331

猜你喜欢

转载自blog.csdn.net/qq_23858785/article/details/96580133
今日推荐