OpenMP是一个跨平台的多线程实现,由主线程生成一系列的子线程,并将任务划分给这些子线程进行执行。这些子线程会并行运行,由运行时环境将线程分配给不同的处理器。
即 主线程–>(生成)子线程 子线程并行运行(分配给不同的处理器)
主流 C++ 编译器,如 gcc、Visual C++ 都支持 OpenMP
现在主流且较新的 Linux 发行版本,使用 gcc/g++ 即可编译 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
//串行执行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等。
编译器发现#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做累加,于是出现了错误。
那么怎么用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>
上面的代码虽然达到了目的,但是它产生了较多的额外操作,比如要先生成数组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虽然很方便,但它只支持一些基本操作,比如 + 、- 、* 、& 、| 、 && 、 || 等。
但是有些情况下,我们需要避免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问题,但显而易见,执行速度会变低,因为可能存在线程等待情况。