当前位置: 首页 > 工具软件 > openMP > 使用案例 >

OpenMP的介绍及简单使用(1)

松和泰
2023-12-01

OpenMP的介绍及简单使用

OpenMP是一个跨平台的多线程实现,由主线程生成一系列的子线程,并将任务划分给这些子线程进行执行。这些子线程会并行运行,由运行时环境将线程分配给不同的处理器。

主线程–>(生成)子线程 子线程并行运行(分配给不同的处理器)

主流 C++ 编译器,如 gcc、Visual C++ 都支持 OpenMP

现在主流且较新的 Linux 发行版本,使用 gcc/g++ 即可编译 OpenMP 程序。

1、利用OpenMP并行特点测试计算机内核数


$ vim test.cpp
#include<omp.h>			//声明引用OpenMP库
#include<iostream>	      //基本输入输出流
int main()
{
    std::cout << "parallel begin:\n"; 
    
    #pragma omp parallel
    //parallel表示其后{}中的语句将被多个线程并行执行
    {
        //omp_get_thread_num()返回当前线程id.
        std::cout << omp_get_thread_num();
    }
    std::cout << "\n parallel end.\n";
    std::cin.get();
    return 0;
}

"#pragma omp parallel"会使大括号中的代码块并行执行。

	#pragma omp parallel
        {
            //每个线程都会执行大括号里的代码
        }

运行结果:

[root@red f]# g++ test.cpp -fopenmp
[root@red f]# ./a.out
parallel begin:
62534701   			//第一次运行打印出的线程id
parallel end.
[root@red f]# ./a.out
parallel begin:
74150623			//第二次运行打印出的线程id
parallel end.

两次运行中每次有8个id被打印出来,说明此计算机的内核数量为8。

但是两次打印的id顺序不一样,这说明多个线程的执行顺序是不确定的

Tips: 在使用gcc编译时需使用编译选项 -fopenmp,如果不使用-fopenmp可能会在链接时报错: “undefined reference to `omp_get_thread_num’ ”
eg. g++ -fopenmp filename.cpp -o filename

2、并行与串行执行for循环的比较

//串行执行for循环

#include <iostream>
#include <omp.h>
void test()
{
    int a = 0;
    for (int i=0;i<100000000;i++)
        a++;
}
int main()
{
    double time = omp_get_wtime();
    //利用omp_get_wtime()函数可以测量执行的时间,单位秒
    for (int i=0;i<8;i++)
        test();
    std::cout<<"time = "<< omp_get_wtime()-time << " seconds"<< std::endl;
}

运行结果如下:

[root@red-hat f]# g++ time.cpp -fopenmp
[root@red-hat f]# ./a.out
time = 1.75524 seconds

下面在main函数中,用“ #pragma omp parallel for ”把上面的编程并行执行

//利用OpenMP并行执行for循环

#include <iostream>
#include <omp.h>
void test()
{
    int a = 0;
    for (int i=0;i<100000000;i++)
        a++;
}
int main()
{
    double time = omp_get_wtime();
    //利用omp_get_wtime()函数可以测量执行的时间,单位秒
    #pragma omp parallel for //将下面的for循环并行执行
    for (int i=0;i<8;i++)
        test();     //多个线程并行执行test()
    std::cout<<"time = "<< omp_get_wtime()-time << " seconds"<< std::endl;
}

运行结果如下:

[root@red f]# g++ timeMP.cpp -fopenmp
[root@red f]# ./a.out
time = 0.276875 seconds

由运行结果可知,运行时间从1.755 秒减少到0.276秒,相比串行执行并行执行时间减少到近1/7,大大降低了运行的时间。

OpenMP for指示将C++ for循环的多次迭代划分给多个线程(划分指,每个线程执行的迭代互不重复,所有线程的迭代并起来正好是C++ for循环的所有迭代),这里C++ for循环需要一些限制从而能在执行C++ for之前确定循环次数,例如C++ for中不应含有break等。

3、竞态条件(race condition)

编译器发现#pragma omp parallel for后,自动将下面的for循环分成N份,(N为电脑CPU核数),然后把每份指派给一个核去执行,且多个核之间为并行执行。

#include <iostream>
int main()
{
#pragma omp parallel for //并行执行下面的for语句
    for (int i=0;i<10;i++)
        std::cout<<i;
     std::cout<<"\n";
    return 0;
}
//运行结果
[root@red-hat jt]# g++ num.cpp -fopenmp -o num
[root@red-hat jt]# ./num
0123648795
[root@red-hat jt]# ./num
9476012385
[root@red-hat jt]# ./num
2743869501
[root@red-hat jt]# ./num
6012357498

我们会发现控制台打印出的数字都不一样,这是因为每个输出语句之间是并行执行,所以每次执行时打印出来的顺序可能不一样。

竞态条件的概念

当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。导致竞态条件发生的代码区称作临界区。

当多个线程并行执行时,有可能多个线程同时对某变量进行了读写操作,从而导致不可预知的结果。比如下面的例子,对于包含10个整型元素的数组a,用for循环求它各个元素之和,并将结果保存在变量sum里。

下面举一个竞态条件发生的例子:

//串行执行:
#include <iostream>
int main()
{
    int sum = 0;
    int a[10] = {1,2,3,4,5,6,7,8,9,10};
    for (int i=0;i<10;i++)
        sum = sum + a[i];
    std::cout<<"sum: "<<sum<<std::endl;
    return 0;
}
//串行执行结果
[root@red-hat jt]# vi race.cpp
[root@red-hat jt]# g++ race.cpp -fopenmp -o race
[root@red-hat jt]# ./race
sum: 55

//并行执行:
#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
    for (int i=0;i<10;i++)
        sum = sum + a[i];
    std::cout<<"sum: "<<sum<<std::endl;
    return 0;
}
//并行执行结果
[root@red-hat jt]# g++ race.cpp -fopenmp -o race
[root@red-hat jt]# ./race
sum: 55
[root@red-hat jt]# ./race
sum: 55
[root@red-hat jt]# ./race
sum: 47
[root@red-hat jt]# ./race
sum: 55
[root@red-hat jt]# ./race
sum: 46

从上面的例子可以看到,按照并行方式执行后,sum可能会变成其他值,比如在第3次运行时,sum=47。其原因是,当某线程A执行sum = sum + a[i]的同时,另一线程B正好更新了sum,而此时A还在用旧的sum做累加,于是出现了错误。

4、用OpenMP实现并行数组求和

那么怎么用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循环
    for (int i=0;i<10;i++)
    {
        int k = omp_get_thread_num(); //获得每个线程的ID
        sumArray[k] = sumArray[k]+a[i];//使各个线程更新自己线程对应的sumArray里的元素
    }
    for (int i = 0;i<coreNum;i++)    //将上面的sumArray中的各元素相加
        sum = sum + sumArray[i]; 	
    std::cout<<"sum: "<<sum<<std::endl;//输出结果
    return 0;
}

//运行结果
[root@red-hat jt]# vi arr.cpp
[root@red-hat jt]# g++ arr.cpp -fopenmp -o arr
[root@red-hat jt]# ./arr
sum: 55

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

5、利用归约并行执行for循环

上面的代码虽然达到了目的,但是它产生了较多的额外操作,比如要先生成数组sumArray,最后还要用一个串行的for循环将sumArray的各元素累加起来,比较麻烦。

有没有简单的方法呢?OpenMP为我们提供了另一个工具,归约(reduction)

例子如下:

#include <iostream>
int main(){
    int sum = 0;
    int a[10] = {1,2,3,4,5,6,7,8,9,10};
#pragma omp parallel for reduction(+:sum) //利用归约
/*
  omp parallel for reduction(+:sum)的意思是告诉编译器:
  下面的for循环要分成多个线程运行,但每个线程都要保存一个自己的变量sum
  在循环结束后,所有线程把自己的sum累加起来作为最后的sum输出。
*/
    for (int i=0;i<10;i++)
        sum = sum + a[i];
    std::cout<<"sum: "<<sum<<std::endl;
    return 0;
}
//运行结果:
[root@red-hat jt]# g++ arrre.cpp -fopenmp -o arrre
[root@red-hat jt]# ./arrre
sum: 55

reduction虽然很方便,但它只支持一些基本操作,比如 + 、- 、* 、& 、| 、 && 、 || 等。

6、利用critical并行执行for循环

但是有些情况下,我们需要避免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里面的语句
  但当执行到critical里面时,要注意有没有其他线程正在里面执行,
  如果有的话,要等其他线程执行完再进去执行。
*/
    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;
}

//运行结果:
[root@red-hat jt]# g++ max.cpp -fopenmp -o max
[root@red-hat jt]# ./max
max: 689

在上例中,避免了race condition问题,但显而易见,执行速度会变低,因为可能存在线程等待情况。

 类似资料: