Tags: C/C++

一、OpenMP用途与介绍

OpenMP提供了对并行算法的高层的抽象描述,程序员通过在源代码中加入专用的pragma来指明自己的意图,由此编译器可以自动将程序进行并行化,并在必要之处加入同步互斥以及通信。 但是,作为高层抽象,OpenMP并不适合需要复杂的线程间同步和互斥的场合。 OpenMP的另一个缺点是不能在非共享内存系统(如计算机集群)上使用,在这样的系统上,MPI使用较多。

目前CPU普遍是4核以上,这对于计算密集型的程序来说是个好消息。在没有GPU加速的情况下,可以考虑使用CPU多线程来加速。 这在如图像处理等领域有较大价值。OpenMP就是这样一个并发编程的框架,支持的编程语言包括C语言、C++和Fortran,支持OpenMP的编译器包括Sun Studio,Intel Compiler,Microsoft Visual Studio,GCC。 在VS中使用OpenMP非常简单,只需要在项目的“属性->C/C++->语言->OpenMP支持”设置中启用OpenMP即可。

二、OpenMP的用法

在OpenMP中使用parallel指令表示并行代码段,如下:

# pragma omp parallel
{
	//每个线程都会执行当前代码块
}

默认情况下,并行区内线程数=系统中核的个数。 并行区里每个线程都会去执行并行区中的代码。 故对于双核电脑,并行区中的代码会被执行2次,当然若有输出语句,结果也会被输出2次 。

并行区里每个线程执行的代码是一样的,计算机若有N个核,相当于同时重复执行了N次,并没有提高效率、节省时间。 我们希望的是把同一工作分配给不同线程来做,每个线程完成一部分,这样运行速率才会快。这就需要对for并行了。 而对于for循环,可以使用for制导语句,将for循环分配给各个线程执行,这里要求数据不存在依赖。 当编译器发现#pragma omp parallel for后,自动将下面的for循环分成N份,(N为电脑CPU核数),然后把每份指派给一个核去执行,而且多核之间为并行执行。 形式为:

//第一种
# pragma omp parallel for
for()

//第二种
# pragma omp parallel
//注意:大括号必须要另起一行
{
	//第二种形式中并行块里面不要再出现parallel制导指令
	# pragma omp for
	for()
}

第一种形式作用域只是紧跟着的那个for循环,而第二种形式在整个并行块中可以出现多个for制导指令。 同时需要注意的是如果不使用for制导语句,则每个线程都执行整个for循环。 所以,应使用for制导语句将for循环拆分开来,从而尽可能平均地分配到各个线程执行。 注意区分每个线程都跑一遍全部的for循环,和for循环拆分到各个线程运行的区别。

当两个并行块之间的代码没有并行的时候,默认是由主线程执行。如下代码。

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

using namespace std;

int main()
{
#pragma omp parallel for  
	for (int i = 0; i < 6; i++)
		printf("i = %d, I am Thread %d\n", i, omp_get_thread_num());
	
	//这里是两个for循环之间的代码,将会由线程0即主线程执行
	printf("I am Thread %d\n", omp_get_thread_num());

#pragma omp parallel for  
	for (int i = 0; i < 6; i++)
		printf("i = %d, I am Thread %d\n", i, omp_get_thread_num());
	system("pause");
}

两个for循环中间的printf()函数就是由主线程执行的。 但如果用第二种写法把for循环写进parallel并行块中就需要注意。 由于用parallel标识的并行块中每一行代码都会被多个线程处理,所以如果想让两个for循环之间的代码由一个线程执行的话就需要在代码前用single或master制导语句标识。 master由是主线程执行,single是选一个线程执行,到底选哪个线程不确定。

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

using namespace std;

int main()
{
#pragma omp parallel  
{

#pragma omp for
		for (int i = 0; i < 6; i++)
		printf("i = %d, I am Thread %d\n", i, omp_get_thread_num());

#pragma omp master  
		{
			//这里的代码由主线程执行
			printf("I am Thread %d\n", omp_get_thread_num());
		}

#pragma omp for  
		for (int i = 0; i < 6; i++)
			printf("i = %d, I am Thread %d\n", i, omp_get_thread_num());
}
	system("pause");
}

OpenMP 对可以多线程化的循环有如下五个要求:

  • 循环的变量变量(就是i)必须是有符号整形,其他的都不行。
  • 循环的比较条件必须是< <= > >=中的一种。
  • 循环的增量部分必须是增减一个不变的值(即每次循环是不变的)。
  • 如果比较符号是< <=,那每次循环i应该增加,反之应该减小。
  • 循环必须是没有奇奇怪怪的东西,不能从内部循环跳到外部循环,goto和break只能在循环内部跳转,异常必须在循环内部被捕获。

如果你的循环不符合这些条件,那就只好改写了。

基本上每个循环都会读写数据,确定哪些数据是线程之间共有的,那些数据是线程私有的就是程序员的责任了。 当数据被设置为公有的时候,所有的线程访问的都是相同的内存地址,当数据被设为私有的时候,每个线程都有自己的一份拷贝。 默认情况下,除了循环变量以外,所有数据都被设定为公有的。可以通过以下两种方法把变量设置为私有的:

  • 在循环内部声明变量,注意不要是static的
  • 通过OpenMP指令声明私有变量

示例代码如下:

// 1. 在循环内部声明变量
#pragma omp parallel for
for (int i = 0; i < 100; i++) {
    int temp = array[i];
    array[i] = doSomething(temp);
}

// 2. 通过OpenMP指令说明私有变量
int temp;
#pragma omp parallel for private(temp)
for (int i = 0; i < 100; i++) {
    temp = array[i];
    array[i] = doSomething(temp);
}

private/firstprivate/lastprivate都是子句,用于表示并行区域内的变量的数据范围属性。 其中,private表示并行区域内的每一个线程都会产生一个并行区域外同名变量的共享变量,且和共享变量没有任何关联; firstprivaet在private的基础上,在进入并行区域时(或说每个线程创建时,或副本变量构造时),会使用并行区域外的共享变量进行一次初始化工作; lastprivate在private的基础上,在退出并行区域时,会使用并行区域内的副本的变量,对共享变量进行赋值,由于有多个副本,OpenMP规定了如何确定使用哪个副本进行赋值。 另外,private不能和firstprivate/lastprivate混用于同一个变量,firstprivate和lastprivate可以对同一变量使用,效果为两者的结合。 threadprivate是指令,和private的区别在于,private是针对并行区域内的变量的,而threadprivate是针对全局的变量的。

此外可以包含omp.h,从而使用更多OpenMP相关函数。

  • omp_get_num_procs():获取处理器个数
  • omp_get_thread_num():获得每个线程的ID
  • omp_set_num_threads():设置并行线程数

三、应用实例

读取两幅影像并进行特征点匹配。传统串行代码如下:

#include "opencv2/highgui/highgui.hpp"
#include "opencv2/features2d/features2d.hpp"
#include <iostream>
#include <omp.h>
int main( ){
    cv::SurfFeatureDetector detector( 400 );    
    cv::SurfDescriptorExtractor extractor;
    cv::BruteForceMatcher<cv::L2<float> > matcher;
    std::vector< cv::DMatch > matches;
    cv::Mat im0,im1;
    std::vector<cv::KeyPoint> keypoints0,keypoints1;
    cv::Mat descriptors0, descriptors1;
    double t1 = omp_get_wtime( );
    //先处理第一幅图像
    im0 = cv::imread("rgb0.jpg", CV_LOAD_IMAGE_GRAYSCALE );
    detector.detect( im0, keypoints0);
    extractor.compute( im0,keypoints0,descriptors0);
    std::cout<<"find "<<keypoints0.size()<<"keypoints in im0"<<std::endl;
    //再处理第二幅图像
    im1 = cv::imread("rgb1.jpg", CV_LOAD_IMAGE_GRAYSCALE );
    detector.detect( im1, keypoints1);
    extractor.compute( im1,keypoints1,descriptors1);
    std::cout<<"find "<<keypoints1.size()<<"keypoints in im1"<<std::endl;
    double t2 = omp_get_wtime( );
    std::cout<<"time: "<<t2-t1<<std::endl;
    matcher.match( descriptors0, descriptors1, matches );
    cv::Mat img_matches;
    cv::drawMatches( im0, keypoints0, im1, keypoints1, matches, img_matches ); 
    cv::namedWindow("Matches",CV_WINDOW_AUTOSIZE);
    cv::imshow( "Matches", img_matches );
    cv::waitKey(0);
    return 1;
}

但由于每一幅影像的读取、提取特征点、计算描述子的过程是相互独立的,可以并行处理。因此可以改写成如下代码:

#include "opencv2/highgui/highgui.hpp"
#include "opencv2/features2d/features2d.hpp"
#include <iostream>
#include <omp.h>
int main( )
{
    cv::SurfFeatureDetector detector( 400 );    
	cv::SurfDescriptorExtractor extractor;
    cv::BruteForceMatcher<cv::L2<float> > matcher;
    std::vector< cv::DMatch > matches;
    cv::Mat im0,im1;
    std::vector<cv::KeyPoint> keypoints0,keypoints1;
    cv::Mat descriptors0, descriptors1;
    double t1 = omp_get_wtime( );
#pragma omp parallel sections
    {
#pragma omp section
        {
            std::cout<<"processing im0"<<std::endl;
            im0 = cv::imread("rgb0.jpg", CV_LOAD_IMAGE_GRAYSCALE );
            detector.detect( im0, keypoints0);
            extractor.compute( im0,keypoints0,descriptors0);
            std::cout<<"find "<<keypoints0.size()<<"keypoints in im0"<<std::endl;
        }
#pragma omp section
        {
            std::cout<<"processing im1"<<std::endl;
            im1 = cv::imread("rgb1.jpg", CV_LOAD_IMAGE_GRAYSCALE );
            detector.detect( im1, keypoints1);
            extractor.compute( im1,keypoints1,descriptors1);
            std::cout<<"find "<<keypoints1.size()<<"keypoints in im1"<<std::endl;
        }
    }
    double t2 = omp_get_wtime( );
    std::cout<<"time: "<<t2-t1<<std::endl;
    matcher.match( descriptors0, descriptors1, matches );
    cv::Mat img_matches;
    cv::drawMatches( im0, keypoints0, im1, keypoints1, matches, img_matches ); 
    cv::namedWindow("Matches",CV_WINDOW_AUTOSIZE);
    cv::imshow( "Matches", img_matches );
    cv::waitKey(0);
    return 1;
}

parallel sections里面的内容要并行执行,具体分工上,每个线程执行其中的一个section,如果section数大于线程数,那么就等某线程执行完它的section后,再继续执行剩下的section。

本文作者原创,未经许可不得转载,谢谢配合

返回顶部