OpenMP基础知识详解及代码示例,学习OpenMP看这里就够了!

1. OpenMP基本介绍

        OpenMP是一个编译器指令和库函数的集合,主要是为共享式存储计算机上的并行程序设计使用的。目前支持OpenMP的语言主要有Fortran,C/C++。

1.1 fork/join并行执行模式的概念

        OpenMP在并行执行程序时,采用的是fork/join式并行模式,共享存储式并行程序就是使用fork/join式并行的。在开始时,只有一个叫做主线程的运行线程存在 。在运行过程中,当遇到需要进行并行计算的时候,派生出(Fork)线程来执行并行任务 。在并行代码结束执行,派生线程退出或挂起,控制流程回到单独的主线程中(Join)。

OpenMP基础知识详解及代码示例,学习OpenMP看这里就够了!_第1张图片

        如图,标准并行模式执行代码的基本思想是,程序开始时只有一个主线程,程序中的串行部分都由主线程执行,并行的部分是通过派生其他线程来执行,但是如果并行部分没有结束时是不会执行串行部分的,先看一个简单例子:

void test()
{
     int a = 0;
     clock_t t1 = clock();
     for (int i = 0; i < 100000000; i++)
     {
         a = i+1;
     }
     clock_t t2 = clock();
     printf("Time = %d\n", t2-t1);
}
int main(int argc, char* argv[])
{
     clock_t t1 = clock();
#pragma omp parallel for
     for ( int j = 0; j < 2; j++ ){
         test();
     }
     clock_t t2 = clock();
     printf("Total time = %d\n", t2-t1);
     test();
     return 0;
}

        在main()函数中,没有执行完for循环中的代码之前,后面的clock_t t2 = clock();这行代码是不会执行的,如果和调用线程创建函数相比,它相当于先创建线程,并等待线程执行完,所以这种并行模式中在主线程里创建的线程并没有和主线程并行运行。

1.2 性能举例

        如上一节的代码,在test()函数中,执行了1亿次循环,主要是用来执行一个长时间的操作。在main()函数里,先在一个循环里调用test()函数,只循环2次,我们看一下在双核CPU上的运行结果吧:

        Time = 297
        Time = 297
        Total time = 297
        Time = 297

        可以看到在for循环里的两次test()函数调用都花费了297ms, 但是打印出的总时间却只花费了297ms,后面那个单独执行的test()函数花费的时间也是297ms,可见使用并行计算后效率提高了整整一倍。

1.3 编译器对OpenMP的支持

        目前支持OpenMP的C/C++编译器主要有微软的VC和Intel的C/C++编译器。
        在微软的VC8.0中,要将项目的属性对话框中“配置属性”下的“C/C++”下的“语言”页里,将“OpenMP支持”由否改为“是/(openmp)”即可。

对于Intel的C/C++编译器,则需要在编译命令行参数里使用对应的编译参数。参数设置 windows下 “/Qopenmp”; Linux下 “-openmp”,在项目中配置链接参数:

        “type=‘win32’ name=‘Microsoft VC80.OpenMP’ version=‘8.0.50727.42’ processorArchitecture=‘X86’ publicKeyToken=‘1fc8b3b9a1e18e3b’”

        当前的Visual Studio完全支持OpenMP 2.0标准 .通过新的编译器选项 /openmp来支持OpenMP程序的编译和链接 .使用Visual Stidio 编写OpenMP程序 如下:

OpenMP基础知识详解及代码示例,学习OpenMP看这里就够了!_第2张图片

2. OpenMP编程模型

2.1 OpenMP指令和库函数介绍

        下面来介绍OpenMP的基本指令和常用指令的用法,在C/C++中,OpenMP指令使用的格式为

#pragma omp 指令 [子句[子句]]

        前面提到的parallel for就是一条指令,有些书中也将OpenMP的“指令”叫做“编译指导语句”,后面的子句是可选的。例如:

#pragma omp parallel private(i, j)

parallel 就是指令, private是子句。为叙述方便把包含#pragma和OpenMP指令的一行叫做语句,如上面那行叫parallel语句。

2.2 OpenMP指令列表

这里我们先列举出OpenMP常用的指令和函数,并附上一些简单的说明。如果你看不懂,没关系,后面我们会对每个指令有详细的例子介绍。

  • parallel,用在一个代码段之前,表示这段代码将被多个线程并行执行

  • for,用于for循环之前,将循环分配到多个线程中并行执行,必须保证每次循环之间无相关性。

  • parallel for, parallel 和 for语句的结合,也是用在一个for循环之前,表示for循环的代码将被多个线程并行执行。

  • sections,用在可能会被并行执行的代码段之前

  • parallel sections,parallel和sections两个语句的结合

  • critical,用在一段代码临界区之前

  • single,用在一段只被单个线程执行的代码段之前,表示后面的代码段将被单线程执行。

  • flush, 用来保证线程的内存临时视图和实际内存保持一致,即各个线程看到的共享变量是一致的

  • barrier,用于并行区内代码的线程同步,所有线程执行到barrier时要停止,直到所有线程都执行到barrier时才继续往下执行。

  • atomic,用于指定一块内存区域被制动更新

  • master,用于指定一段代码块由主线程执行

  • ordered, 用于指定并行区域的循环按顺序执行

  • threadprivate , 用于指定一个变量是线程私有的

  • copyprivate:配合single指令,将指定线程的专有变量广播到并行域内其他线程的同名变量中;

  • copyin n:用来指定一个threadprivate类型的变量需要用主线程同名变量进行初始化;

  • default:用来指定并行域内的变量的使用方式,缺省是shared。

2.3 OpenMP库函数

        OpenMP除上述指令外,还有一些库函数,OpenMP运行时库函数原本用以设置和获取执行环境相关的信息.其也包含一系列用以同步的API.要使用运行时函数库所包含的函数,应该在相应的源文件中包含OpenMP头文件omp.h.OpenMP的运行时库函数的使用类似于相应编程语言内部的函数调用.

        由编译指导语句和运行时库函数可见,OpenMP同时结合了两种并行编程的方式,通过编译指导语句,可以将串行的程序逐步地改造成一个并行程序,达到增量更新程序的目的,从而减少程序编写人员的一定负担。同时,这样的方式也能将串行程序和并行程序保存在同一个源代码文件当中,减少了维护的负.OpenMP在运行的时候,需要运行函数库的支持,并会获取一些环境变量来控制运行的过程。环境变量是动态函数库中用来控制函数运行的一些参数.

        下面列出几个常用的库函数:

  • omp_get_num_procs, 返回运行本线程的多处理机的处理器个数。

  • omp_get_num_threads, 返回当前并行区域中的活动线程个数。

  • omp_get_thread_num, 返回线程号

  • omp_set_num_threads, 设置并行执行代码时的线程个数

  • omp_init_lock , 初始化一个简单锁

  • omp_set_lock, 上锁操作

  • omp_unset_lock, 解锁操作,要和omp_set_lock函数配对使用。

  • omp_destroy_lock, omp_init_lock函数的配对操作函数,关闭一个锁

2.3 OpenMP子句

  • private, 指定每个线程都有它自己的变量私有副本。

  • firstprivate,指定每个线程都有它自己的变量私有副本,并且变量要被继承主线程中的初值。

  • lastprivate,主要是用来指定将线程中的私有变量的值在并行处理结束后复制回主线程中的对应变量。

  • reduce,用来指定一个或多个变量是私有的,并且在并行处理结束后这些变量要执行指定的运算。

  • nowait,忽略指定中暗含的等待

  • num_threads,指定线程的个数

  • schedule,指定如何调度for循环迭代

  • shared,指定一个或多个变量为多个线程间的共享变量

  • ordered,用来指定for循环的执行要按顺序执行

  • copyprivate,用于single指令中的指定变量为多个线程的共享变量

  • copyin,用来指定一个threadprivate的变量的值要用主线程的值进行初始化。

  • default,用来指定并行处理区域内的变量的使用方式,缺省是shared

3. OpenMP详细代码示例

        备注:本教程默认屏幕前面的您对C/C++、Visual Studio甚至Linux都已经比较熟悉了。

3.1 hello_openmp.cpp

/* 尝试着在编译选项里使用和不使用-openmp 这个编译选项分别编译并执行代码 */
#include 

int main()
{
#ifdef _OPENMP  // 如果定义了这个宏
	std::cout << "Hello, OpenMP!" << std::endl;
#else
	std::cout << "OpenMP is not enabled." << std::endl;
#endif
	return 0;
}

3.2 header_and_env.cpp

/*  引入头文件,OpenMP的几乎所有函数定义都在这个头文件中。 */
#include 
#include   // 包含OpenMP头文件 

int main()
{
	// omp_get_max_threads() 等其它函数都定义在omp.h头文件中
	// omp_get_max_threads() 获取本机的CPU线程数
	std::cout << "OpenMP will use " << omp_get_max_threads() <<
	    " threads maximum." << std::endl;
	return 0;
}

3.3 parallel.cpp

/*
  1. 尝试将环境变量改为 OMP_NUM_THREADS=2 和 OMP_NUM_THREADS=3 再编译运行程序试试
  2. 尝试在 #pragma omp parallel 后添加num_threads(5) 试试
*/
#include 
#include 

int main()
{
#pragma omp parallel   // OpenMP 并行区域
{
	// 花括号里的内容会被N个线程同时执行,N定义在环境变量OMP_NUM_THREADS中 
	printf("Hello from thread %d of %d\n", omp_get_thread_num(), omp_get_num_threads());
}
	return 0;
}

3.4 parallel_cout.cpp

/*
   程序说明: 我们会发现打印在控制台的内容是乱的,这是因为在一个线程还没输出完成时,另一个线程就抢着要输出了!
*/
#include 
#include 
#include 

int main()
{
#pragma omp parallel
{
	std::cout << "Hello from thread " << omp_get_thread_num() << " of " <<  omp_get_num_threads() << std::endl;
}
	return 0;
}

3.5 nested.cpp

/*
    1. 看看打印输出是否跟您想的一样,如果不一样,为什么?
    2. 试着禁止嵌套并行;
*/
#include 
#include 

int  main()
{
	omp_set_nested(1);   // 允许嵌套并行可用 

#pragma omp parallel num_threads(2)
{
	printf("Level 1, thread %d of %d\n", omp_get_thread_num(),omp_get_num_threads());
	
	#pragma omp parallel num_threads(2)
	{
		printf("Level 2, thread %d of %d\n", omp_get_thread_num(),omp_get_num_threads());
	}
}
	return 0;
}

3.6 parallel-for.cpp

/*
    for循环的并行
*/
#include 

int main()
{
	const int size = 50;
	int a[size];

#pragma omp parallel for
	for (int i = 0; i < size; i++)
		a[i] = i;                     // 这里的代码是并行执行的

	for (int i = 0; i < size; i++)    // 这里是串行执行的,#pragma omp parallel for作用范围只有紧接着的for循环,当然这个for循环是可以嵌套的.
		std::cout << a[i] << std::endl;

	return 0;
}

3.7 scoping.cpp

/*
   1. 变量的作用范围在并行程序设计中非常重要
   2. 如果在并行区域再加一个私有的a变量,想想会发生什么?
*/
#include 
#include 

int main()
{
	int a = -1;
#pragma omp parallel
{
	// a在并行块内部是共享(默认)的,所有线程都有权操作它(操作的都是同一个变量,没有备份),而且并行快结束后,块内代码对其的修改有效
	int b;     // 在并行区域外不可见,每个线程有一个备份拷贝
	a = omp_get_thread_num() + 100;
	b = omp_get_thread_num() + 200;
}
	std::cout << "a = " << a << std::endl;   // 理论上这里的输出是[100+0,100+threads)之间随机的, 得看哪个线程最后执行完
    //	b = 0;   // 对外不可见,这里会发生错误,所以注释
	return 0;
}

3.8 firstprivate.cpp

/*
    1. 试试如果把private改为firstprivate会发生什么?
    2. 说明:firstprivate的作用是,让i默认使用并行区域外i的值来初始化并行区域内的私有i,但是初始化后并行去内部的i就跟外面的没有关系了,各个线程仍然持有一个i的私有备份,运行结束时,原有的i值保持i=10不变.
*/
#include 
#include 

int main()
{
	int i = 10;

#pragma omp parallel private(i)
{
	// 私有变量i是覆盖了并行区域外的共享变量i,所以这里并没有初始化,值应该为0
	printf("thread %d, i = %d\n", omp_get_thread_num(), i);
	i = 200 + omp_get_thread_num();
}
	printf("i = %d\n", i);
	return 0;
}

3.9 lastprivate.cpp

/*
    跟firstprivate相反,lastprivate主要是用来指定将线程中的私有变量的值在并行处理结束后复制回主线程中的对应变量。
*/
#include 

int main()
{
	const int size = 1000;
	int i = -1, a[size];

#pragma omp parallel for lastprivate(i)
	for (i = 0; i < size; i++)
		a[i] = i;

	std::cout << "i = " << i << std::endl;
	return 0;
}

3.10 single-master-critical.cpp

/*
  1. omp critical -> execute by one thread at a time
  2. omp single   -> execute by any one thread
  3. omp master   -> execute by the master thread (id == 0)

*/
#include 
#include 

int main()
{
#pragma omp parallel num_threads(8)
{
#pragma omp critical
	std::cout << "Hello from thread " << omp_get_thread_num() << " of " <<
	    omp_get_num_threads() << std::endl;
}
	return 0;
}

3.11 mutex.cpp

/*
    1. 锁是多线程计算里非常重要的概念,他是保证数据一致性的基础,没有锁的并行计算会导致非常奇怪的结果。
    2. 举个栗子:当多个线程操作一个数据时,一个线程在读取一个数的时候另两个线程在对这个数作写操作,那这个读线程到底应该拿哪一个数值去做计算呢?回答当然是,拿最后更改这个值的线程的值去计算,但是这个值就是对的么?
    3. 很多时候即使是并行的,但是对于一个简单操作来讲,它也是需要有先后循序的,比如这个线程在操作这个变量的时候要求别的线程要等待这个线程操作完成,这时候就使用锁将该白能量锁住。
    4. 如果没有听懂我这里在讲什么,请参考百度相关资料进行学习,这里只是展示OpenMP中锁的运用。
*/
#include 
#include 
#include 

int main()
{
	omp_lock_t lock;
	omp_init_lock(&lock);

#pragma omp parallel num_threads(4)
{
	omp_set_lock(&lock); // mutual exclusion (mutex)
	std::cout << "Thread " << omp_get_thread_num() <<  " has acquired the lock. Sleeping 2 seconds..." << std::endl;
	sleep(2);
	std::cout << "Thread " << omp_get_thread_num() <<" is releasing the lock..." << std:: endl; omp_unset_lock(&lock);
}
	omp_destroy_lock(&lock);
	return 0;
}

3.12 barrier.cpp

/*
   1. 同步也是并行计算中特别重要的概念,跟上面讲的锁一样;
   2. 特别是在时间相关的计算领域里,如含时的有限差分等等;
   3. 因为在具体程序中,每个线程执行的任务不一样,即使执行的任务一样,也不能保证每个线程执行任务消费的时间都完全一致。有的线程已经执行了5行代码,有的线程才执行到第0行。而含时的迭代需要所有线程都执行完t步骤后,才能继续执行t+1时间步,不然会导致错误的结果。
   4. 不太理解的可以参考其他资料学习,或者自己多踩一些并行计算不同步的坑就很明了了。
*/
#include 
#include 

int main()
{
#pragma omp parallel
{
	printf("Hello from thread %d of %d\n", omp_get_thread_num(),
	    omp_get_num_threads());
#pragma omp barrier //  所有的线程都执行到这里时才能继续往后执行 
	printf("Thread %d of %d have passed the barrier\n",
	    omp_get_thread_num(), omp_get_num_threads());
}
	return 0;
}

3.13 atomic.cpp

/*
   1. 原子变量跟锁有相近似的作用,都是保证变量或者事务的一致性;
   2. 举个栗子:银行转账,A给B转账过程中突然停电,A账户前丢失了B却没有收到,这是谁的责任?当然是银行的责任。当然,银行的程序员们可不是吃白米饭的,转账的操作就是一个原子操作,只有成功或失败,没有成功了一半这个说法。
   3. 微软装系统是的回滚操作也是这样,系统安装出错给你提供一个回滚的功能,保证你回到之前正常的使用状态。
*/
#include 
#include 

inline double two_body_energy(int i, int j)
{
	return (2.0 * i + 3.0 * j) / 10.0; // some dummy return value
}

int main()
{
	const int nbodies = 1000;
	double energy = 0.0;

#pragma omp parallel
	for (int i = 0; i < nbodies; i++) {
		for (int j = i+1; j < nbodies; j++) {
			double eij = two_body_energy(i, j);
			energy += eij;
		}
	}

	std::cout << "energy = " << energy << std::endl;
	return 0;
}

// XXX: 在外层循环中使用并行
// XXX: 为什么结果是错误的?
// XXX: 尝试使用 #pragma omp atomic 修正这个错我

3.14 reduction.cpp

/*
   归约的概念请参考相关资料,向量元素求和、阶乘问题就属于典型的归约问题。
   1. 常见的归约运算符: (* - && || max min ...)
   2. 归约运算时请特别注意变量的作用范围
   3. 试着改变energy变量的初始值,看会发生什么?
   4. 把energy变量改为shared,试试看会发生什么?
*/
#include 
#include 

double
two_body_energy(int i, int j)
{
	return (2.0 * i + 3.0 * j) / 10.0; // some dummy return value
}

int main()
{
	const int nbodies = 1000;
	double energy = 0.0;

// + 是进行加运算
// energy 是要进行归约的变量
#pragma omp parallel for reduction(+:energy)   
	for (int i = 0; i < nbodies; i++) {
		for (int j = i+1; j < nbodies; j++) {
			double eij = two_body_energy(i, j);
			energy += eij;
		}
	}

	std::cout << "energy = " << energy << std::endl;
	return 0;
}

3.15 scheduling.cpp

       schedule只能用于循环并行构造中,其作用是用于控制循环并行结构的任务调度。一个简单的理解,一个for循环假设有10次迭代,使用4个线程去执行,那么哪些线程去执行哪些迭代呢?可以通过schedule去控制迭代的调度和分配,从而适应不同的使用情况,提高性能。

  • static -> 大部分的编译器实现,在没有使用schedule子句的时候,系统就是采用static方式调度的。

        对于schedule(static,size)的含义,OpenMP会给每个线程分配size次迭代计算。这个分配是静态的,“静态”体现在这个分配过程跟实际的运行是无关的,可以从逻辑上推断出哪几次迭代会在哪几个线程上运行。具体而言,对于一个N次迭代,使用M个线程,那么,[0,size-1]的size次的迭代是在第一个线程上运行,[size, size + size -1]是在第二个线程上运行,依次类推。那么,如果M太大,size也很大,就可能出现很多个迭代在一个线程上运行,而某些线程不执行任何迭代。需要说明的是,这个分配过程就是这样确定的,不会因为运行的情况改变,比如,我们知道,进入OpenMP后,假设有M个线程,这M个线程开始执行的时间不一定是一样的,这是由OpenMP去调度的,并不会因为某一个线程先被启动,而去改变for的迭代的分配,这就是静态的含义。

  • dynamic -> 每个线程运行结束时获得新的计算任务

        动态调度迭代的分配是依赖于运行状态进行动态确定的,所以哪个线程上将会运行哪些迭代是无法像静态一样事先预料的。对于dynamic,没有size参数的情况下,每个线程按先执行完先分配的方式执行1次循环,比如,刚开始,线程1先启动,那么会为线程1分配一次循环开始去执行(i=0的迭代),然后,可能线程2启动了,那么为线程2分配一次循环去执行(i=1的迭代),假设这时候线程0和线程3没有启动,而线程1的迭代已经执行完,可能会继续为线程1分配一次迭代,如果线程0或3先启动了,可能会为之分配一次迭代,直到把所有的迭代分配完。所以,动态分配的结果是无法事先知道的,因为我们无法知道哪一个线程会先启动,哪一个线程执行某一个迭代需要多久等等,这些都是取决于系统的资源、线程的调度等等。

  • guided -> 类似动态钓鱼,但是 chunk size是自适应的。

        类似于动态调度,但每次分配的循环次数不同,开始比较大,以后逐渐减小。size表示每次分配的迭代次数的最小值,由于每次分配的迭代次数会逐渐减少,较少到size时,将不再减少。如果不知道size的大小,那么默认size为1,即一直减少到1。具体是如何减少的,以及开始比较大(具体是多少?),参考相关手册的信息。

  • auto -> 编译器动态决定采用那栋策略

        runtime表示根据环境变量确定上述调度策略中的某一种,默认也是静态的(static)。

        控制schedule环境变量的是OMP_SCHEDULE环境变量,其值和上面的三中类型一样了,比如:

        setenv OMP_SCHEDULE “dynamic, 5” 就是schedule(dynamic,5)的含义了。

#include 
#include 

#define CHUNK_SIZE 5

// scheduling:


int main()
{
	const int niter = 25;

#pragma omp parallel for schedule(static, CHUNK_SIZE)
	for (int i = 0; i < niter; i++) {
		int thr = omp_get_thread_num();
		printf("iter %d of %d on thread %d\n", i, niter, thr);
	}

	return 0;
}

3.16 ordered.cpp

/*
   在循环代码中某些代码的执行需要按规定的顺序执行,比如在一个循环中,一部分的工作可以并行执行,而特定的部分需要按照串行的工作流程依次执行。
*/
#include 
#include 

int main()
{
	const int niter = 10;
#pragma omp parallel for ordered       // 这里必须这么写
	for (int i = 0; i < niter; i++) {
		int thr = omp_get_thread_num();
		printf("unordered iter %d of %d on thread %d\n", i, niter, thr);
#pragma omp ordered                    // 这里是需要顺序执行的部分
		printf("ordered iter %d of %d on thread %d\n", i, niter, thr);
	}
	return 0;
}

3.17 loop-dependencies.cpp

/*

*/
#include 
#include 

int
main()
{
	const int n = 10000;
	long a[n];

	for (int i = 0; i < n; i++)
		a[i] = i;
		
#pragma omp parallel
	for (int i = 1; i < n; i++)
		a[i] += a[i-1];

	std::cout << a[n-1] << std::endl;
	return 0;
}

// XXX: Parallelize the second loop.
// XXX: Why is the result of parallel program incorrect? How to fix it?

3.18 sections.cpp

/*
     1. 有些需要并行的任务并不是一个for循环之类的,而是一个个代码块,这种情况下就可以使用sections的情形;
     2. sections下包含多个section,section相互之间只并行执行的,但是section内部是串行执行的;
     3. 多个sections之间也是串行执行的
     4. 如果#pragma omp parallel sections 写成 #pragma omp sections,则各个section之间是串行执行的 
     5. 尝试num_threads(2)会发生什么?
*/
#include 
#include 
#include 

int  main()
{
#pragma omp parallel sections num_threads(4)
{
#pragma omp section // 独立线程
	{
		int thr = omp_get_thread_num();
		printf("section 1, thread %d - sleeping 1 second\n", thr);
		sleep(1);
		printf("section 1 done\n");
	}
#pragma omp section // 独立线程
	{
		int thr = omp_get_thread_num();
		printf("section 2, thread %d - sleeping 2 second\n", thr);
		sleep(2);
		printf("section 2 done\n");
	}
#pragma omp section // 独立线程
	{
		int thr = omp_get_thread_num();
		printf("section 3, thread %d - sleeping 3 second\n", thr);
		sleep(3);
		printf("section 3 done\n");
	}
	// printf("not in omp section"); // error - code must be in section
}
	return 0;
}

// XXX: what happens when we change num_threads to 2

3.19 threadprivate.cpp

        threadprivate指令用来指定全局的对象被各个线程各自复制了一个私有的拷贝,即各个线程具有各自私有的全局对象。threadprivate和private的区别在于threadprivate声明的变量通常是全局范围内有效的,而private声明的变量只在它所属的并行构造中有效。用作threadprivate的变量的地址不能是常数。对于C++的类(class)类型变量,用作threadprivate的参数时有些限制,当定义时带有外部初始化时,必须具有明确的拷贝构造函数。程序示例如下:

int g;
#pragma omp threadprivate(g)       //一定要先声明
int main(int argc, char *argv[])
{
       /* Explicitly turn off dynamic threads */
       omp_set_dynamic(0);

#pragma omp parallel
       {
              g = omp_get_thread_num();   
              printf("tid: %d\n",g);         //随机依次输出0~3
       } // End of parallel region


#pragma omp parallel
       {
              int temp = g*g;
              printf("tid : %d, tid*tid: %d\n",g, temp);  //不同线程中全局变量值不同
       } // End of parallel region

}

        注意:在使用threadprivate的时候,要用omp_set_dynamic(0)关闭动态线程的属性,才能保证结果正确。

3.20 Copyin.cpp

        copyin子句用于将主线程中threadprivate变量的值拷贝到执行并行区域的各个线程的threadprivate变量中,从而使得team内的子线程都拥有和主线程同样的初始值。程序示例如下:

#include  
int A = 100; 

#pragma omp threadprivate(A) 
int main(int argc, _TCHAR* argv[]) 
{ 
#pragma omp parallel for 
    for(int i = 0; i<10;i++) 
    { 
        A++; 
        printf("Thread ID: %d, %d: %d\n",omp_get_thread_num(), i, A);   // #1 
    } 
    printf("Global A: %d\n",A); // 并行区域外的打印的“Globa A”的值总是和前面的thread 0的结果相等,因为退出并行区域后,只有master线程即0号线程运行。
#pragma omp parallel for copyin(A)
    for(int i = 0; i<10;i++) 
    { 
        A++; 
        printf("Thread ID: %d, %d: %d\n",omp_get_thread_num(), i, A);   // #1 
    } 
    printf("Global A: %d\n",A); // #2 
    return 0; 
}

3.21 Copyprivate.cpp

        copyprivate子句用于将线程私有副本变量的值从一个线程广播到执行同一并行区域的其他线程的同一变量。copyprivate只能用于single指令(single指令:用在一段只被单个线程执行的代码段之前,表示后面的代码段将被单线程执行)的子句中,在一个single块的结尾处完成广播操作。copyprivate只能用于private/firstprivate或threadprivate修饰的变量。程序示例如下:

int counter = 0;

#pragma omp threadprivate(counter)
int increment_counter()
{
         counter++;
         return(counter);
}

#pragma omp parallel
         {
                   int    count;
#pragma omp single copyprivate(counter)
                   {
                            counter = 50;
                   }

                   count = increment_counter();
                   printf("ThreadId: %ld, count = %ld/n", omp_get_thread_num(), count);

}

3.22 nowait.cpp

        栅障(Barrier)是OpenMP用于线程同步的一种方法。线程遇到栅障是必须等待,直到并行区中的所有线程都到达同一点。注意:在任务分配for循环和任务分配section结构中,我们已经隐含了栅障,在parallel,for,sections,single结构的最后,也会有一个隐式的栅障。

        隐式的栅障会使线程等到所有的线程继续完成当前的循环、结构化块或并行区,再继续执行后面的工作。可以使用nowait去掉这个隐式的栅障.去掉隐式栅障,例如:

 #pragma omp parallel //并行区内
        {
           #pragma omp for nowait // 任务分配for循环
                  for(k=0;k<m;k++){
                       fun1(k);
                   }
           #pragma omp sections private(y,z)
             {
                   #pragme omp section//任务分配section
                       {y=sectionA(x);}
                   #pragme omp section
                       {z=sectionB(x);}
             }                   
        }

        因为第一个 任务分配for循环和第二个任务分配section代码块之间不存在数据相关。加上显示栅障,例如:

#pragma omp parallel shared(x,y,z) num_threads(2)//使用的线程数为2
      {
          int tid=omp_get_thread_num();
          if(tid==0)
              y=fun1();//第一个线程得到y
          else 
               z=fun2();//第二个线程得到z
          #pragma omp barrier //显示加上栅障,保证y和z在使用前已有值
          #pragma omp for
                  for(k=0;k<100;k++)
                          x[k]=y+z;
      }

        单线程和多线程交错执行: 当开发人员为了减少开销而把并行区设置的很大时,有些代码很可能只执行一次,并且由一个线程执行,这样单线程和多线程需要交错执行

        举例如下:

#pragma omp parallel //并行区
     {
           int tid=omp_get_thread_num();//每个线程都调用这个函数,得到线程号
            //这个循环被划分到多个线程上进行
             #pragma omp for nowait
             for(k=0;k<100;k++)
                   x[k]=fun1(tid);//这个循环的结束处不存在使所有线程进行同步的隐式栅障
           #pragma omp master
             y=fn_input_only(); //只有主线程会调用这个函数
           #pragma omp barrier   //添加一个显示的栅障对所有的线程同步,从而确保x[0-99]和y处于就绪状态
            //这个循环也被划分到多个线程上进行
           #pragma omp for nowait
             for(k=0;k<100;k++)
                x[k]=y+fn2(x[k]); //这个线程没有栅障,所以不会相互等待
            //一旦某个线程执行完上面的代码,不需要等待就可以马上执行下面的代码
            #pragma omp single //注意:single后面意味着有隐式barrier
            fn_single_print(y);
             //所有的线程在执行下面的函数前会进行同步
            #pragma omp master
            fn_print_array(x);//只有主线程会调用这个函数
     } 

3.23 taskdepend.cpp

        参考网址: https://blog.csdn.net/augusdi/article/details/8807683

3.24 taskloop.cpp

        参考网址:http://www.lupaworld.com/article-259566-1.html

3.25 flush.cpp

当并行区域里存在一共享变量,并且对其进行修改时,需要用flush更新变量,确保并行的多线程对共享变量的读操作是最新值.

done=0;
#pragma omp flush(done)
 if(!done)
 {
    ...
    done=1;
 }

4. 总结

        没错,OpenMP就是这么简单。您看到这里,您已经可以解决使用OpenMP解决大部分并行计算任务了。

5. 参考资料

  1. OpenMP 4.5 新特性: https://www.oschina.net/translate/what-is-new-in-openmp-4-5-3?cmp
  2. OpenMP 任务依赖:https://stackoverflow.com/questions/27475174/task-dependency-in-openmp-4
  3. OpenMP 官方主页:https://www.openmp.org/
  4. OpenMP 官方代码示例:https://www.openmp.org/wp-content/uploads/openmp-examples-4.5.0.pdf

你可能感兴趣的:(高性能计算,编程语言)