L
O
A
D
I
N
G

OpenMP


OpenMP

OpenMP介绍

在C/C++中,OpenMP可以通过使用预处理指令来让程序并行化。OpenMP指令使用的格式为:

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

下面是一个最简单的OpenMP程序,可以运行后观察结果与普通程序有什么不同。

请在适当的位置填上#pragma omp parallel for 使程序并行执行。

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

int main(int argc, char* argv[])
{
    int i;
	// YOUR CODE HERE
	#pragma omp parallel for 
	// END OF YOUR CODE
	for (i = 0; i < 10; i++) {
		printf("i = %d\n", i);
	}
	return 0;
}

运行结果

i = 2
i = 0
i = 6
i = 1
i = 8
i = 5
i = 4
i = 7
i = 9
i = 3

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

OpenMP是一套用于共享内存并行系统的多处理器程序设计的指导性的编译处理方案,从之前的介绍我们可以发现程序还是在循环结束之后才运行return 0语句,因此可以推断OpenMP并行执行的程序要全部结束后才会运行后面非并行部分的代码,这就是fork/join并行模式。以上结论可以从示例代码中体现。

请在适当的位置填上#pragma omp parallel for 使程序并行执行。

#pragma omp parallel for

运行结果

Time = 159
Time = 161
Total time = 162
#include <stdio.h>
#include <omp.h>

void foo()
{
    int cnt = 1e8;
}

int main(int argc, char* argv[])
{
    double t1 = omp_get_wtime();
	// your code
    #pragma omp parallel for
    
    for (int i = 0; i < 2; i++) {
        foo();
    }

    double t2 = omp_get_wtime();
    printf("Total time = %f seconds\n", t2 - t1);
    
    return 0;
}

parallel 指令

#pragma omp parallel 是 OpenMP 中用于创建并行区域(parallel region)的指令,它可以搭配 forsections 以及其他子句来实现不同的并行操作。

这个指令的作用是创建一个并行区域,在这个区域中的代码可以被多个线程同时执行,以充分利用多核处理器的性能优势。

下面是对 #pragma omp parallel 的详细解释和示例用法:

  • 解释

    • #pragma omp 是 OpenMP 指令的前缀,用于指示编译器执行并行化操作。
    • parallel 指令用于创建一个并行区域,其中的代码可以由多个线程同时执行。
    • 在并行区域中的代码将会被多个线程执行,每个线程都将执行相同的代码。每个线程都将执行这段代码,并行地进行计算,直到并行区域结束为止。
    • 在并行区域内部的变量具有不同的作用域。比如在每个线程中,变量的值是独立的,每个线程都有自己的变量实例。
    • 默认情况下,线程数量由系统决定,但也可以通过设置 OMP_NUM_THREADS 环境变量或调用 omp_set_num_threads() 函数来指定线程数量。
  • 作用

    • 创建一个并行区域,使其中的代码能够在多个线程中并行执行,充分利用多核处理器的性能。
    • 用于并行化一段代码,使其能够在多个线程中同时执行,加快程序的执行速度。
  • 用法

    • 需要注意的是,#pragma omp parallel 只是创建一个并行区域,不会自动对代码进行并行化。在这个并行区域中的代码将被多个线程执行,但如果没有进一步的指示,代码仍然是串行执行的。因此,为了实现真正的并行化,需要结合其他 OpenMP 指令(比如 for 循环的 parallel forparallel sections)来并行执行具体的任务。

    • 与一段代码块配合使用,可以是复合语句、函数或一个 for 循环。

    • #pragma omp parallel 可以与不同的子句一起使用,比如 forsections,并可以添加其他子句来调整并行区域的行为。

    • 示例1:与 for 结合使用

      #pragma omp parallel for
      for (int i = 0; i < N; i++) {
          // 并行执行的 for 循环代码
          // ...
      }

      这种用法将一个 for 循环中的迭代分配给多个线程并行执行。

    • 示例2:与 sections 结合使用

      #pragma omp parallel sections
      {
          #pragma omp section
          {
              // 第一个部分的代码
              // ...
          }
          
          #pragma omp section
          {
              // 第二个部分的代码
              // ...
          }
      }

      这种用法将不同的代码部分分配给不同的线程并行执行,每个 section 内的代码将在独立的线程中执行。

  • 其他子句

    • 可以在 #pragma omp parallel 后添加其他子句来调整并行区域的行为。比如设置线程数量、私有变量等等。例如:

      #pragma omp parallel num_threads(4) private(x)
      {
          // ...
      }
      • num_threads(4):指定并行区域中的线程数量为4。
      • private(x):指定变量 x 在每个线程中是私有的,每个线程有自己的 x 变量实例。

#pragma omp parallel 创建的并行区域中的代码将会在多个线程间并行执行。在并行区域中的变量可能具有不同的作用域和共享性质,需要小心处理共享变量可能引发的竞态条件和数据同步问题。

超算习堂

parallel 是构造并行块的一个指令,同时也可以配合其他指令如for, sections等指令一起使用。在这个指令后面需要使用一对大括号来指定需要并行计算的代码。

#pragma omp parallel [for | sections] [子句[子句]…] 
{ 
//并行部分 
}

通过实例代码我们可以看出并行部分创建出了多个线程来完成。

请在适当的位置填上#pragma omp parallel num_threads(6) 使程序并行执行。

运行结果

Thread: 0
Thread: 2
Thread: 1
Thread: 3
Thread: 4
Thread: 5
#include <stdio.h>
#include <omp.h>

int main(int argc, char* argv[])
{
	// YOUR CODE HERE
	#pragma omp parallel num_threads(6) 
	// END OF YOUR CODE
	{
		printf("Thread: %d\n", omp_get_thread_num());
	}
	return 0;
}

for 指令

在 OpenMP 中,for 循环通常与 #pragma omp parallel for 结合使用,将一个 for 循环中的迭代任务分配给多个线程并行执行。用于在循环中实现并行化,允许将一个普通的for循环转换为一个并行化的for循环,从而提高程序的执行效率。这样可以有效地利用多核处理器的性能,加速循环中的计算。

以下是对 #pragma omp parallel for 的详细解释和使用方法:

  • 语法

    #pragma omp parallel for
    for (初始化语句; 循环条件; 更新语句) {
    	// 循环体
    }
  • 作用

    • 将一个 for 循环中的迭代任务分配给多个线程并行执行,加速循环的运行。
    • 通常用于对大型迭代进行并行化处理,例如遍历数组、矩阵运算等。
  • 用法

    • #pragma omp parallel for 用于并行化一个 for 循环,用法示例如下:

      #pragma omp parallel for
      for (int i = 0; i < N; i++) {
          // 并行执行的 for 循环体代码
          // ...
      }

      这个指令将 for 循环中的迭代任务平均地分配给多个线程,每个线程执行一部分迭代。循环迭代会被分成多个块,每个线程执行其中的一个块。

      例如,我们有一个计算数组元素平方和的简单任务,可以使用#pragma omp parallel for将其并行化:

      #include <iostream>
      #include <omp.h>
      
      int main() {
      const int N = 1000;
      double sum = 0.0;
      double arr[N];
      
      // 初始化数组
      for (int i = 0; i < N; ++i) {
        arr[i] = static_cast<double>(i);
      }
      
      // 使用#pragma omp parallel for进行并行化
      #pragma omp parallel for reduction(+:sum)
      for (int i = 0; i < N; ++i) {
        sum += arr[i] * arr[i];
      }
      
      std::cout << "Sum of squares: " << sum << std::endl;
      
      return 0;
      }

      在这个例子中,我们使用了reduction(+:sum)子句来指定如何合并多个线程的结果。这样,每个线程都会计算其部分元素的平方和,并将结果累加到全局变量sum中。最后,主线程会输出整个数组元素平方和的结果。

  • 注意事项

    • 在使用 #pragma omp parallel for 时,循环变量(如上例中的 i)必须是迭代变量。OpenMP 将根据迭代变量自动进行任务分配。
    • 循环的迭代次数应该足够大,以便在多个线程之间实现有效的并行执行。如果循环迭代次数太小,则并行化的开销可能超过了并行执行所带来的性能提升。
  • 其他子句

    • 可以使用其他 OpenMP 子句来进一步控制并行化的行为,例如指定私有变量、指定线程数量、设置线程绑定等。例如:

      #pragma omp parallel for num_threads(4) private(x)
      for (int i = 0; i < N; i++) {
          // ...
      }
      • num_threads(4):指定并行区域中的线程数量为4。
      • private(x):指定变量 x 在每个线程中是私有的,每个线程有自己的 x 变量实例。

#pragma omp parallel for 的使用可以显著提高循环迭代的效率,但需要注意并行化可能引入的数据竞争和同步问题,特别是对于共享变量的操作需要进行合适的同步控制。

超算习堂

for指令的作用是使一个for循环在多个线程中执行,一般for指令会与parallel指令同时使用,即parallel for指令。此外还可以在parallel指令的并行块中单独使用,在一个并行块中可以使用多个for指令。但是单独使用for指令是没有效果的。

请在适当的位置使用for指令使程序并行执行。

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

int main(int argc, char* argv[])
{
    #pragma omp parallel 
    {
        int i, j;
        
        #pragma omp for private(i)
        for (i = 0; i < 5; i++)
            printf("i = %d\n", i);
        
        #pragma omp for private(j)
        for (j = 0; j < 5; j++)
            printf("j = %d\n", j);
    }
    return 0;
}

运行结果

i = 1
i = 0
i = 4
i = 3
i = 2
j = 0
j = 2
j = 1
j = 3
j = 4

sections和section指令

在 OpenMP 中,#pragma omp sections#pragma omp section 是用于并行化代码的指令,允许将不同的代码部分分配给不同的线程并行执行。这种并行化模式允许多个线程同时执行各自的代码段,可以提高程序的效率。

以下是对 #pragma omp sections#pragma omp section 的详细解释和用法:

  • 作用

    • #pragma omp sections 用于创建一个并行区域,内部包含多个 section,每个 section 中的代码可以由不同的线程并行执行。
    • #pragma omp section 用于定义 sections 区域中的一个具体代码段。
  • 用法

    • #pragma omp sections 必须与多个 #pragma omp section 结合使用,代码示例如下:

      #pragma omp parallel
      {
          #pragma omp sections
          {
              #pragma omp section
              {
                  // 第一个 section 的代码
                  // ...
              }
              
              #pragma omp section
              {
                  // 第二个 section 的代码
                  // ...
              }
              
              // 可以有更多的 sections
          }
      }

      这种用法将 sections 区域内的代码分成多个 section,每个 section 中的代码会被不同的线程并行执行。

  • 特点

    • 每个 section 中的代码都是独立执行的,各个 section 之间的执行顺序不确定,取决于系统调度和线程完成任务的时间。
    • sections 区域中的线程数目与 section 的数目无关,OpenMP 会自动决定线程分配情况。
  • 注意事项

    • 每个 section 都应该是互相独立的任务单元,不应该有共享变量的修改或者依赖关系,以免引起竞态条件或死锁等问题。
    • sections 区域中的每个 section 可能会被不同的线程执行,因此要确保每个 section 的执行是独立的,不会相互影响。

#pragma omp sections#pragma omp section 适用于将程序中的不同任务并行化执行,特别适用于任务间没有数据依赖性的情况。通过这种方式,可以提高程序的效率和性能,充分利用多核处理器的优势。

sections语句可以将下面的代码分成不同的分块,通过section指令来指定分块。每一个分块都会并行执行,具体格式:
#pragma omp [parallel] sections [子句]
{
#pragma omp section
{
//代码
}

}

请在适当的位置填上使用sections指令使程序并行执行

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

int main(int argc, char* argv[])
{
    #pragma omp parallel sections
    {
        #pragma omp section 
        {
            printf("Section 1 ThreadId = %d\n", omp_get_thread_num());
            // 某些代码任务
        }

        #pragma omp section
        {
            printf("Section 2 ThreadId = %d\n", omp_get_thread_num());
            // 可能的合并任务或其他处理
        }
    }
    return 0;
}

运行结果

Section 2 ThreadId = 1
Section 1 ThreadId = 0
Section 4 ThreadId = 3
Section 3 ThreadId = 2

private 子句

在 OpenMP 中,private 子句用于指定变量在并行区域中的私有性质。这意味着每个线程都会拥有该变量的一个私有副本,而不是共享同一个变量副本。这样可以避免多个线程同时访问和修改同一个变量引发的竞态条件问题。

以下是对 private 子句的详细解释和用法:

  • 作用

    • 在并行区域中,将变量指定为私有的,以确保每个线程都拥有自己的变量副本。
    • 避免多个线程对同一个变量进行并发读写而导致的数据混乱或不确定的结果。
  • 用法

    • 在 OpenMP 指令中,通过 private 子句来指定私有变量,例如:

      #pragma omp parallel private(i, j)
      {
          // i 和 j 变量在并行区域中是私有的
          // 每个线程都会有自己的 i 和 j 副本
          // ...
      }

      在这个示例中,ij 变量被指定为私有变量,每个线程都会有自己的 ij 的副本。任何在并行区域内声明的变量默认情况下都是共享的,使用 private 子句可以将其设为私有。

  • 作用范围

    • private 子句可以在不同的 OpenMP 指令中使用,比如 parallel, for, sections 等,并且可以在同一个指令块中同时指定多个私有变量。
    • 如果在循环并行化 for 中使用了 private 子句,则循环索引变量通常需要被指定为私有变量。
  • 注意事项

    • 私有变量仅在并行区域中是私有的,出了并行区域后,变量仍然是原来的共享状态。
    • 被指定为私有的变量需要在每个线程中进行初始化。因为每个线程都有自己的变量副本,变量值在不同线程中是独立的。

private 子句是用于控制变量私有性的重要工具,在多线程并行编程中经常用于避免共享变量带来的并发访问问题,确保程序的正确性和可靠性。

private 子句可以将变量声明为线程私有,声明称线程私有变量以后,每个线程都有一个该变量的副本,线程之间不会互相影响,其他线程无法访问其他线程的副本。原变量在并行部分不起任何作用,也不会受到并行部分内部操作的影响。

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

int main(int argc, char* argv[])
{
    int i = 20;
    
    #pragma omp parallel for private(i)
    for (i = 0; i < 10; i++)
    {
        printf("i = %d\n", i);
    }

    // 手动更新循环外的 i
    // 得到最后一个并行循环中的 i 值
    i = 10;

    printf("outside i = %d\n", i);
    return 0;
}

运行结果

i = 0
i = 2
i = 1
i = 4
i = 3
i = 5
i = 6
i = 7
i = 9
i = 8
outside i = 20
firstprivate子句

firstprivate 子句是 OpenMP 中用于指定并行区域中变量的初始私有值的一种指令。它用于保留变量在并行区域外的初始值,并将其作为每个线程私有变量的初始值,以便在并行执行过程中使用。

以下是 firstprivate 子句的用法:

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

int main() {
    int x = 5;
    
    #pragma omp parallel firstprivate(x)
    {
        // x 变量在并行区域中是私有的,并且每个线程都有一个私有副本
        // 初始值是 x 在并行区域外的初始值(5)
        
        // 在并行区域内修改 x 的值
        x += omp_get_thread_num();
        
        printf("Thread %d: x = %d\n", omp_get_thread_num(), x);
    }
    
    // 在并行区域外打印 x 的值,值仍然为初始值(5)
    printf("After parallel region: x = %d\n", x);
    
    return 0;
}

在这个示例中,firstprivate(x) 将变量 x 指定为私有变量,并且每个线程都有自己的私有副本,初始值为并行区域外 x 的初始值(5)。在并行区域内,每个线程将 x 的值增加了当前线程编号 omp_get_thread_num(),然后输出结果。

值得注意的是,虽然并行区域内对 x 进行了修改,但在并行区域外,x 的值仍然保持初始值,不受并行区域内修改的影响。这是因为 firstprivate 子句保留了变量在并行区域外的初始值,并在并行区域内每个线程都有一个私有的初始值副本,不会影响到外部的 x

private子句不能继承原变量的值,但是有时我们需要线程私有变量继承原来变量的值,这样我们就可以使用firstprivate子句来实现。

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

int main(int argc, char* argv[])
{
	int t = 20, i;
	// YOUR CODE HERE
	#pragma omp parallel for firstprivate(t)
	// END OF YOUR CODE
	for (i = 0; i < 5; i++)
	{
		t += i;
		printf("t = %d\n", t);
	}
	printf("outside t = %d\n", t);
	return 0;
}

运行结果

t = 20
t = 21
t = 23
t = 24
t = 22
outside t = 20
lastprivate子句

lastprivate 子句是 OpenMP 中用于指定并行循环或区域结束后,从多个线程中获取最后一个迭代的变量值,并将其赋值给指定变量的一种指令。

以下是 lastprivate 子句的用法:

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

int main() {
    int last_val = 0;
    
    #pragma omp parallel for lastprivate(last_val)
    for (int i = 0; i < 10; ++i) {
        last_val = i;
        printf("Thread %d: i = %d\n", omp_get_thread_num(), i);
    }
    
    // 在并行循环结束后,获取最后一个迭代的值赋给 last_val
    printf("Last value after parallel region: %d\n", last_val);
    
    return 0;
}

在这个示例中,lastprivate(last_val) 将变量 last_val 指定为 lastprivate 变量。在并行循环中,每个线程都有一个私有的 last_val 变量,但最终会从多个线程中获取最后一个迭代的值,并将该值赋给外部的 last_val

请注意,lastprivate 子句只对循环结束后的最后一个迭代有效。在并行循环执行期间,每个线程都会在自己的 last_val 中存储最后一个迭代的值,但只有在并行区域结束后,会将最后一个迭代的值赋给外部的 last_val

这对于需要在并行循环结束后获取最后一个迭代的值并继续使用的情况非常有用,例如需要保留并利用并行循环中最后一个迭代的结果。

除了在进入并行部分时需要继承原变量的值外,有时我们还需要再退出并行部分时将计算结果赋值回原变量,lastprivate子句就可以实现这个需求。
需要注意的是,根据OpenMP规范,在循环迭代中,是最后一次迭代的值赋值给原变量;如果是section结构,那么是程序语法上的最后一个section语句赋值给原变量。
如果是类(class)变量作为lastprivate的参数时,我们需要一个缺省构造函数,除非该变量也作为firstprivate子句的参数;此外还需要一个拷贝赋值操作符。

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

int main(int argc, char* argv[])
{
	int t = 20, i;
	// YOUR CODE HERE
	#pragma omp parallel for firstprivate(t), lastprivate(t)
	// END OF YOUR CODE
	for (i = 0; i < 5; i++)
	{
		t += i;
		printf("t = %d\n", t);
	}
	printf("outside t = %d\n", t);
	return 0;
}

运行结果

t = 20
t = 22
t = 24
t = 21
t = 23
outside t = 24
threadprivate子句

threadprivate 是 OpenMP 中用于指定全局变量在每个线程中具有自己的私有副本的指令。通常,全局变量在并行区域中是共享的,所有线程都能访问和修改它。但有时,我们需要在每个线程中拥有自己的变量副本,以便不同线程之间的修改不会相互影响。

threadprivate 的用法如下所示:

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

// 定义一个全局变量,在每个线程中将拥有自己的私有副本
#pragma omp threadprivate(global_var)

int global_var; // 全局变量

int main() {
    global_var = 10; // 全局变量的初始值
    
    // 启动并行区域,多个线程同时访问 global_var
    #pragma omp parallel
    {
        // 每个线程都会有一个私有的 global_var
        global_var += omp_get_thread_num();
        printf("Thread %d: global_var = %d\n", omp_get_thread_num(), global_var);
    }
    
    return 0;
}

在这个示例中,global_var 是一个全局变量,但使用了 #pragma omp threadprivate(global_var) 指令,因此在每个线程中都会有一个 global_var 的私有副本。每个线程对 global_var 进行操作时都在自己的私有副本上进行,不会相互干扰。

值得注意的是,threadprivate 用于指定全局变量在每个线程中具有自己的私有副本。这样的话,每个线程都有一个与其他线程独立的变量副本,它们互不干扰。

threadprivate子句可以将一个变量复制一个私有的拷贝给各个线程,即各个线程具有各自私有的全局对象。
格式:#pragma omp threadprivate(list)

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

int g = 0;
#pragma omp threadprivate(g)

int main(int argc, char* argv[])
{
	int t = 20, i;
	// YOUR CODE HERE
	#pragma omp parallel
	// END OF YOUR CODE
	{
		g = omp_get_thread_num();
	}
	#pragma omp parallel
	{
		printf("thread id: %d g: %d\n", omp_get_thread_num(), g);
	}
	return 0;
}

运行结果

thread id: 1 g: 1
thread id: 5 g: 5
thread id: 2 g: 2
thread id: 3 g: 3
thread id: 6 g: 6
thread id: 4 g: 4
thread id: 0 g: 0
thread id: 7 g: 7

shared子句

在 OpenMP 中,shared 子句用于指定变量在并行区域中的共享性质。共享变量是所有线程共同访问和修改的变量。使用 shared 子句可以将变量显式地声明为共享变量,以确保所有线程在并行区域中都使用同一个变量副本。

以下是对 shared 子句的详细解释和用法:

  • 作用

    • 在并行区域中将变量指定为共享变量,以确保所有线程都共享同一个变量副本。
    • 允许多个线程同时访问和修改变量的值,适用于线程之间需要共享信息的情况。
  • 用法

    • 在 OpenMP 指令中,通过 shared 子句将变量指定为共享变量,例如:

      #pragma omp parallel shared(x, y)
      {
          // x 和 y 变量在并行区域中是共享的
          // 所有线程都使用同一个 x 和 y 变量副本
          // ...
      }

      在这个示例中,xy 变量被指定为共享变量,这意味着所有线程都会使用相同的 xy 的副本。

  • 默认共享性

    • 在 OpenMP 中,默认情况下,在并行区域内声明的变量通常是共享的。因此,对于全局变量和在并行区域之外声明的变量,默认情况下是共享的。
    • 局部变量默认情况下是私有的,每个线程都有自己的私有副本。
  • 注意事项

    • 共享变量可能存在并发访问的问题,需要注意线程之间的同步控制,避免出现数据竞争、不确定行为等问题。
    • 如果变量在并行区域外被声明为共享的,需确保对其的访问是线程安全的,避免多个线程同时修改造成的问题。

shared 子句用于控制变量在并行区域中的共享性质,可帮助开发者更清晰地表达并行代码中的共享数据。但需要特别注意共享变量的同步和互斥访问问题,以确保并行程序的正确性和可靠性。

shared 子句是 OpenMP 中的一个指令,用于指定变量在并行区域中的共享性质。在并行区域内,shared 子句使得变量在不同线程之间共享同一个内存位置。

以下是 shared 子句的用法示例:

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

int main() {
    int shared_var = 0;
    
    // 使用 shared 子句将 shared_var 变量指定为共享变量
    #pragma omp parallel shared(shared_var)
    {
        shared_var += 1; // 所有线程共享同一个 shared_var 变量
        
        // 获取当前线程号和 shared_var 的值
        printf("Thread %d: shared_var = %d\n", omp_get_thread_num(), shared_var);
    }
    
    // 在并行区域结束后,输出最终 shared_var 的值
    printf("Final value of shared_var: %d\n", shared_var);
    
    return 0;
}

在这个示例中,#pragma omp parallel shared(shared_var)shared_var 变量指定为共享变量,这意味着在并行区域中,所有线程都将共享同一个 shared_var 变量。

在并行区域内,每个线程都可以访问并修改 shared_var 的值。因此,在示例中,每个线程都对 shared_var 执行了加一操作,并输出了当前线程号和 shared_var 的值。需要注意的是,由于所有线程共享同一个 shared_var,可能会发生竞争条件,需要谨慎处理并发修改。

最终输出的 Final value of shared_var 表示并行区域结束后的 shared_var 的值。由于多个线程共享该变量,因此最终结果取决于并发修改的顺序和操作。

Share子句可以将一个变量声明成共享变量,并且在多个线程内共享。需要注意的是,在并行部分进行写操作时,要求共享变量进行保护,否则不要随便使用共享变量,尽量将共享变量转换为私有变量使用。

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

int main(int argc, char* argv[])
{
    int t = 20, i;
    
    #pragma omp parallel for shared(t) private(i)
    for (i = 0; i < 10; i++)
    {
        if (i % 2 == 0) {
            #pragma omp atomic
            t++;
        }
        printf("i = %d, t = %d\n", i, t);
    }
    
    return 0;
}

运行结果

i = 0, t = 21
i = 2, t = 22
i = 9, t = 23
i = 4, t = 23
i = 8, t = 25
i = 5, t = 25
i = 7, t = 24
i = 1, t = 24
i = 3, t = 24
i = 6, t = 24

reduction子句

reduction 子句是 OpenMP 中用于对并行循环中的变量执行归约操作的一种指令。归约操作指将并行循环中多个线程的局部结果合并为一个最终结果的过程。这种操作通常用于对变量进行累加、求和、求积等聚合运算,以避免竞态条件并提高并行程序的性能。

以下是对 reduction 子句的详细解释和用法:

  • 作用

    • 在并行循环中对指定的变量执行归约操作,将多个线程的局部结果合并为一个最终结果。
    • 常用于求和、求积、求最大值、最小值等聚合操作。
  • 用法

    • 在 OpenMP 中,reduction 子句用于声明进行归约操作的变量,例如:

      int sum = 0;
      #pragma omp parallel for reduction(+:sum)
      for (int i = 0; i < N; ++i) {
          sum += array[i];
      }

      在这个示例中,reduction(+:sum) 表示对变量 sum 执行归约操作,使用 + 操作符对局部结果进行累加。

  • 支持的操作符

    • reduction 子句支持多种操作符,常用的包括 +*-&|^&&||maxmin 等。具体支持的操作符取决于编译器和 OpenMP 的实现。
  • 特点

    • reduction 子句会自动为每个线程创建一个局部变量,每个线程对局部变量进行归约操作,最后将所有局部变量的结果合并得到最终结果。
    • 在并行循环结束后,变量的值会被存储在原始变量中。
  • 示例

    • 对数组中元素求和的示例代码:

      #include <stdio.h>
      #include <omp.h>
      
      int main() {
          int sum = 0;
          int array[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 += array[i];
          }
      
          printf("Sum = %d\n", sum);
          return 0;
      }

      在这个示例中,reduction(+:sum) 表示对变量 sum 执行累加操作,将各个线程的局部累加结果最后合并到 sum 中。

reduction 子句是一个强大的工具,能够简化对并行循环中变量的归约操作,提高并行程序的性能,减少了手动同步和合并的复杂度。

reduction 子句是 OpenMP 中用于执行并行归约操作的关键指令。它允许在并行循环中对指定的变量执行归约操作,并将各个线程的局部结果合并为一个最终结果。

以下是 reduction 子句的用法示例:

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

int main() {
    int sum = 0;
    
    // 使用 reduction 子句对 sum 变量执行归约操作
    #pragma omp parallel for reduction(+:sum)
    for (int i = 1; i <= 10; ++i) {
        sum += i;
        printf("Thread %d: sum = %d\n", omp_get_thread_num(), sum);
    }
    
    // 在并行循环结束后,输出最终的 sum 值
    printf("Final sum value after reduction: %d\n", sum);
    
    return 0;
}

在这个示例中,#pragma omp parallel for reduction(+:sum)sum 变量指定为 + 归约运算符的归约变量。在并行循环中,每个线程都有自己的 sum 变量,并行地对其进行累加操作。

关键点是 reduction(+:sum) 中的 + 表示进行求和操作,这意味着各个线程各自计算自己的局部和,并在循环结束时将所有局部和合并为一个最终的总和。这种归约操作确保了并行计算的正确性,最终得到了正确的总和结果。

reduction 子句可以用于多种归约操作,如求和、乘积、最大值、最小值等。它提供了一种简洁而有效的方法来在并行计算中执行归约操作,以提高程序性能并确保计算的准确性。

reduction子句可以对一个或者多个参数指定一个操作符,然后每一个线程都会创建这个参数的私有拷贝,在并行区域结束后,迭代运行指定的运算符,并更新原参数的值。
私有拷贝变量的初始值依赖于redtution的运算类型。
具体用法如下
reduction(operator:list)

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

int main(int argc, char* argv[])
{
	
	int i, sum = 10;
	// YOUR CODE HERE
	#pragma omp parallel for reduction(+: sum)
	// END OF YOUR CODE
	for (i = 0; i < 10; i++)
	{
		sum += i;
		printf("%d\n", sum);
	}
	printf("sum = %d\n", sum);
	return 0;
}

运行结果

0
2
1
5
4
5
7
6
8
9
sum = 55

copyin子句

copyin 子句是 OpenMP 中的一个指令,用于将数据从主线程复制到所有线程的私有副本中。它确保了并行区域中所有线程都使用相同的初始数据。

以下是 copyin 子句的简单示例:

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

int main() {
    int initial_data = 10;
    
    // 使用 copyin 子句将 initial_data 数据复制到并行区域中的所有线程的私有副本
    #pragma omp parallel copyin(initial_data)
    {
        // 获取每个线程的私有副本的初始数据值
        printf("Thread %d: initial_data = %d\n", omp_get_thread_num(), initial_data);
    }
    
    return 0;
}

在这个示例中,#pragma omp parallel copyin(initial_data)initial_data 变量的值复制到所有线程的私有副本中,以确保在并行区域中每个线程都有相同的初始数据。

并行区域内的每个线程都可以访问私有副本的 initial_data,并且这些私有副本都具有相同的初始值。在示例中,每个线程打印了自己的线程编号和 initial_data 的值,这些值都是从主线程复制到各自私有副本的。

需要注意的是,copyin 子句通常用于确保并行区域中所有线程拥有相同的初始数据。

copyin子句可以将主线程中变量的值拷贝到各个线程的私有变量中,让各个线程可以访问主线程中的变量。
copyin的参数必须要被声明称threadprivate,对于类的话则并且带有明确的拷贝赋值操作符。

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

int g = 0;
#pragma omp threadprivate(g) 
int main(int argc, char* argv[])
{
	int i;
	#pragma omp parallel for   
	for (i = 0; i < 4; i++)
	{
		g = omp_get_thread_num();
		printf("thread %d, g = %d\n", omp_get_thread_num(), g);
	}
	printf("global g: %d\n", g);
	// YOUR CODE HERE
	#pragma omp parallel for copyin(g)
	// END OF YOUR CODE
	for (i = 0; i < 4; i++)
		printf("thread %d, g = %d\n", omp_get_thread_num(), g);
	return 0;
}

运行结果

thread 0, g = 0
thread 1, g = 1
thread 2, g = 2
thread 3, g = 3
global g: 0
thread 1, g = 0
thread 2, g = 0
thread 0, g = 0
thread 3, g = 0

static字句

在 OpenMP 中,static 字句用于指定并行循环中迭代变量的存储方式。它决定了迭代变量的存储分配方式和范围。

parallel for 循环中,迭代变量可以采用不同的存储方式,其中之一就是使用 static 字句。static 字句指定了在并行循环中,迭代变量的存储分配是静态的,即在编译时就进行分配。

以下是一个示例,展示了 static 字句的用法:

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

int main() {
    int i;

    // 使用 static 字句指定迭代变量 i 的存储分配方式为静态
    #pragma omp parallel for schedule(static)
    for (i = 0; i < 8; i++) {
        printf("Thread %d: i = %d\n", omp_get_thread_num(), i);
    }

    return 0;
}

在这个示例中,#pragma omp parallel for schedule(static) 中的 static 字句指定了迭代变量 i 的存储分配方式为静态。这意味着在编译时,迭代变量 i 的内存空间将被分配,并在每个线程中都保持相同的内存位置。

static 字句还可以与 schedule 指定循环迭代方式一起使用,比如 schedule(static, chunk_size),这样可以进一步控制循环迭代的分块大小。

需要注意的是,static 字句是 OpenMP 中常用的一种迭代变量存储方式,它适用于并行循环中迭代变量访问频率较为均匀的情况。

当parallel for没有带schedule时,大部分情况下系统都会默认采用static调度方式。假设有n次循环迭代,t个线程,那么每个线程大约分到n/t次迭代。这种调度方式会将循环迭代均匀的分布给各个线程,各个线程迭代次数可能相差1次。用法为schedule(method)。

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

int main(int argc, char* argv[])
{
	int i;
	// YOUR CODE HERE
	#pragma omp parallel for schedule(static)
	// END OF YOUR CODE
	for (i = 0; i < 10; i++)
	{
		printf("i = %d, thread %d\n", i, omp_get_thread_num());
	}
	return 0;
}

运行结果

i = 0, thread 0
i = 4, thread 2
i = 1, thread 0
i = 7, thread 5
i = 8, thread 6
i = 2, thread 1
i = 9, thread 7
i = 3, thread 1
i = 5, thread 3
i = 6, thread 4
Size参数的用法

在 OpenMP 的 schedule 指令中,size 参数用于指定分块的大小(chunk size),它影响了并行循环中迭代的分配方式。

在循环并行化中,通常迭代是分配给不同线程的。size 参数控制了每个线程获取的迭代次数。

schedule 指定为 static 或者 dynamic 时,size 参数可以用于控制分块的大小。当 schedule 设置为 static 时,size 指定的是每个线程被分配的迭代次数。在 schedule(static, size) 中,size 表示每个线程被分配的连续迭代次数。

示例如下:

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

int main() {
    int i;

    // 使用 static 分配迭代
    #pragma omp parallel for schedule(static, 3)
    for (i = 0; i < 10; i++) {
        printf("Thread %d: i = %d\n", omp_get_thread_num(), i);
    }

    return 0;
}

在上述示例中,#pragma omp parallel for schedule(static, 3) 指定了静态调度,每个线程被分配了大小为 3 的连续迭代。这样,10 个迭代会被分为三块(3, 3, 3, 1)分配给线程。

需要注意的是,size 参数的具体效果取决于实际问题的特征和硬件环境。在选择 size 参数时,应该根据问题的特性、数据大小和计算资源等因素进行调整,以获得最佳的性能。

在静态调度的时候,我们可以通过指定size参数来分配一个线程的最小迭代次数。指定size之后,每个线程最多可能相差size次迭代。可以推断出[0,size-1]的迭代是在第一个线程上运行,依次类推。

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

int main(int argc, char* argv[])
{
	int i;
	// YOUR CODE HERE
	#pragma omp parallel for schedule(static, 3)
	// END OF YOUR CODE
	for (i = 0; i < 10; i++)
	{
		printf("i = %d, thread %d\n", i, omp_get_thread_num());
	}
	return 0;
}

运行结果

i = 6, thread 2
i = 3, thread 1
i = 4, thread 1
i = 0, thread 0
i = 5, thread 1
i = 9, thread 3
i = 1, thread 0
i = 7, thread 2
i = 2, thread 0
i = 8, thread 2

dynamic子句指令

在 OpenMP 中,schedule 指令可用于指定并行循环中迭代的调度方式。schedule(dynamic, chunk_size) 是其中一种调度方式,其中的 dynamic 子句指示编译器以动态方式调度循环的迭代,chunk_size 表示每次分配给线程的迭代数量。

下面是一个示例,演示了 schedule(dynamic, chunk_size) 的用法:

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

int main() {
    int i;

    // 使用 dynamic 子句和 chunk_size 为 2 进行循环的动态调度
    #pragma omp parallel for schedule(dynamic, 2)
    for (i = 0; i < 10; i++) {
        printf("Thread %d: i = %d\n", omp_get_thread_num(), i);
    }

    return 0;
}

在这个示例中,#pragma omp parallel for schedule(dynamic, 2) 指定了动态调度,每个线程会按需获取两个迭代。当一个线程完成它所分配的两个迭代后,它将获取更多的迭代。这种方式可在并行循环中实现更灵活的负载平衡,因为线程会动态地获取迭代,避免了静态分配中可能出现的某些线程处理的工作量较少而其他线程处理的工作量较多的情况。

需要注意的是,schedule(dynamic, chunk_size) 中的 chunk_size 决定了每次分配给线程的迭代数量。过小的 chunk_size 可能会增加调度开销,因此需要根据实际问题的特性和硬件环境进行调整以获取更好的性能。

动态分配是将迭代动态分配到各个线程,依赖于运行你状态来确定,所以我们无法像静态调度一样事先预计进程的分配。哪一个线程先启动,哪一个线程迭代多久,这些都取决于系统的资源和线程的调度。

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

int main(int argc, char* argv[])
{
	int i;
	// YOUR CODE HERE
	#pragma omp parallel for schedule(dynamic)
	// END OF YOUR CODE
	for (i = 0; i < 10; i++)
	{
		printf("i = %d, thread %d\n", i, omp_get_thread_num());
	}
	return 0;
}

运行结果

i = 0, thread 0
i = 3, thread 3
i = 8, thread 3
i = 9, thread 7
i = 2, thread 1
i = 4, thread 4
i = 5, thread 5
i = 7, thread 0
i = 1, thread 2
i = 6, thread 6

omp_get_num_procs

返回调用函数时可用的处理器数目。
函数原型
int omp_get_num_procs(void)

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

int main(int argc, char* argv[])
{
	printf("%d\n", omp_get_num_procs());
	#pragma omp parallel  
	{
	    // YOUR CODE HERE
	    printf("%d\n", omp_get_num_procs());
	    // END OF YOUR CODE
	}
	return 0;
}

运行结果

8
8
8
8
8
8
8
8
8

omp_get_num_threads

返回当前并行区域中的活动线程个数,如果在并行区域外部调用,返回1
int omp_get_num_threads(void)
注意:由于程序并行执行,每次输出的结果可能会有所区别。

运行结果

1
8
8
8
8
8
8
8
8
#include <stdio.h>
#include <omp.h>

int main(int argc, char* argv[])
{
	printf("%d\n", omp_get_num_threads());
	#pragma omp parallel  
	{
	   	// YOUR CODE HERE
		printf("%d\n", omp_get_num_threads());
		// YOUR CODE HERE
	}
	return 0;
}

omp_get_thread_num

返回当前的线程号,注意不要和之前的omp_get_num_threads混淆。
函数原型
int omp_get_thread_num(void)
注意:由于程序并行执行,每次输出的结果可能会有所区别。

运行结果

0
0
2
1
3
4
5
6
7
#include <stdio.h>
#include <omp.h>

int main(int argc, char* argv[])
{
	printf("%d\n", omp_get_thread_num());
	#pragma omp parallel  
	{	    
	    // YOUR CODE HERE
	    printf("%d\n", omp_get_thread_num());
	    // END OF YOUR CODE
	}
	return 0;
}

omp_set_num_threads

设置进入并行区域时,将要创建的线程个数
函数原型
int omp_set_num_threads(void)
注意:由于程序并行执行,每次输出的结果可能会有所区别。

运行结果

0 of 4 threads
2 of 4 threads
1 of 4 threads
3 of 4 threads
#include <stdio.h>
#include <omp.h>

int main(int argc, char* argv[])
{
    // YOUR CODE HERE
    omp_set_num_threads(4);
    // END OF YOUR CODE
	#pragma omp parallel  
	{
		printf("%d of %d threads\n", omp_get_thread_num(), omp_get_num_threads());
	}
	return 0;
}

omp_in_parallel

可以判断当前是否处于并行状态
函数原型
int omp_in_parallel();
注意:由于程序并行执行,每次输出的结果可能会有所区别。

运行结果

0
1
1
1
1
#include <stdio.h>
#include <omp.h>

int main(int argc, char* argv[])
{
	printf("%d\n", omp_in_parallel());
	omp_set_num_threads(4);
	#pragma omp parallel  
	{
	    // YOUR CODE HERE
		printf("%d\n", omp_in_parallel());
	    // END OF YOUR CODE
	}
	return 0;
}

omp_get_max_threads

该函数可以用于获得最大的线程数量,根据OpenMP文档中的规定,这个最大数量是指在不使用num_threads的情况下,OpenMP可以创建的最大线程数量。需要注意的是这个值是确定的,与它是否在并行区域调用没有关系。
注意:由于程序并行执行,每次输出的结果可能会有所区别。

运行结果

8
4
4
4
4
#include <stdio.h>
#include <omp.h>

int main(int argc, char* argv[])
{
    // YOUR CODE HERE
	printf("%d\n", omp_get_max_threads());
	// END OF YOUR CODE
	omp_set_num_threads(4);
	#pragma omp parallel  
	{
		printf("%d\n", omp_get_max_threads());
	}
	return 0;
}

OpenMP中互斥锁

Openmp中有提供一系列函数来进行锁的操作,一般来说常用的函数的下面4个
void omp_init_lock(omp_lock*) 初始化互斥锁
void omp_destroy_lock(omp_lock*) 销毁互斥锁
void omp_set_lock(omp_lock*) 获得互斥锁
void omp_unset_lock(omp_lock*) 释放互斥锁
注意:由于程序并行执行,每次输出的结果可能会有所区别

运行结果

0+
0-
1+
1-
4+
4-
2+
2-
3+
3-
#include <stdio.h>
#include <omp.h>

static omp_lock_t lock;

int main(int argc, char* argv[])
{
    int i;
	omp_init_lock(&lock); 
	#pragma omp parallel for   
	for (i = 0; i < 5; ++i)
	{
	    // YOUR CODE HERE
		omp_set_lock(&lock);
		// END OF YOUR CODE
		printf("%d+\n", omp_get_thread_num());
		printf("%d-\n", omp_get_thread_num());
		// YOUR CODE HERE
		omp_unset_lock(&lock); 
		// END OF YOUR CODE
	}
	omp_destroy_lock(&lock);
	return 0;
}

omp_test_lock

除了之前介绍的4个函数之外,与互斥锁的相关的函数还有一个,用来尝试获得锁。
该函数可以看作是omp_set_lock的非阻塞版本。
函数原型
bool omp_test_lock(omp_lock*)
注意:由于程序并行执行,每次输出的结果可能会有所区别

运行结果

0+
fail to get lock
fail to get lock
fail to get lock
0-
2+
2-
#include <stdio.h>
#include <omp.h>

static omp_lock_t lock;

int main(int argc, char* argv[])
{
    int i;
	omp_init_lock(&lock); 
	#pragma omp parallel for   
	for (i = 0; i < 5; ++i)
	{
	    // YOUR CODE HERE
		if (omp_test_lock(&lock))
		// END OF YOUR CODE
		{
			printf("%d+\n", omp_get_thread_num());
			printf("%d-\n", omp_get_thread_num());
			omp_unset_lock(&lock);
		}
		else
		{
			printf("fail to get lock\n");
		}
	}
	omp_destroy_lock(&lock);
	return 0;
}

omp_set_dynamic

该函数可以设置是否允许在运行时动态调整并行区域的线程数。
函数原型:
void omp_set_dynamic(int)
当参数为0时,动态调整被禁用。
当参数为非0值时,系统会自动调整线程以最佳利用系统资源。
注意:由于程序并行执行,每次输出的结果可能会有所区别。

运行结果

0
1
2
3
#include <stdio.h>
#include <omp.h>

int main(int argc, char* argv[])
{
    int i;
    // YOUR CODE HERE
	omp_set_dynamic(1);
	// END OF YOUR CODE
	#pragma omp parallel for
	for (i = 0; i < 4; i++)
	{
		printf("%d\n", omp_get_thread_num());
	}
	return 0;
}

omp_get_dynamic

该函数可以返回当前程序是否允许在运行时动态调整并行区域的线程数。
函数原型
int omp_get_dynamic()
当返回值为非0时表示允许系统动态调整线程。
当返回值为0时表示不允许。
注意:由于程序并行执行,每次输出的结果可能会有所区别。

运行结果

0
1
1
1
1
#include <stdio.h>
#include <omp.h>

int main(int argc, char* argv[])
{
    int i;
	printf("%d\n", omp_get_dynamic());
	omp_set_dynamic(1);
	#pragma omp parallel for
	for (i = 0; i < 4; i++)
	{
	    // YOUR CODE HERE
		printf("%d\n", omp_get_dynamic());
		// END OF YOUR CODE
	}
	return 0;
}

文章作者: loyeh
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 loyeh !
评论
  目录