什么是 OpenMP 中的 "implicit synchronization"

What is "implicit synchronization" in OpenMP

OpenMP 中的 "implicit synchronization" 到底是什么?您如何识别?我老师是这么说的

#pragma omp parallel
printf(“Hello 1\n”);

具有隐式同步。为什么?你怎么看?

同步是并行处理和 openmp 中的一个重要问题。通常并行处理是异步的。您知道有多个线程正在处理一个问题,但您无法确切知道它们的实际状态、它们正在处理的迭代等。同步允许您控制线程执行。

openmp 中有两种同步:显式和隐式。使用允许创建 barrier: #pragma omp barrier 的特定 openmp 构造完成显式同步。屏障是一种并行构造,只能由所有线程同时通过。因此,在屏障之后,您可以确切地知道所有线程的状态,更重要的是,您可以知道它们完成了多少工作。

隐式同步在两种情况下完成:

  • 在平行区域的末尾。 Openmp 依赖于 fork-join 模型。程序启动时,会创建一个单线程(master thread)。当您通过 #pragma omp parallel 创建并行部分时,会创建多个线程 (fork)。这些线程将并发工作,并在并行部分结束时被销毁 (join)。所以在并行部分的末尾,你有一个同步并且你准确地知道所有线程的状态(他们已经完成了他们的工作)。这就是您给出的示例中发生的情况。并行部分仅包含 printf(),最后,程序在继续之前等待所有线程终止。

  • 在某些 openmp 构造 的末尾,如 #pragma omp for#pragma omp sections,存在隐式屏障。只要所有线程都没有达到屏障,任何线程都不能继续工作。这对于准确了解不同线程完成了哪些工作很重要。

例如,考虑以下代码。

#pragma omp parallel
{
  #pragma omp for
  for(int i=0; i<N; i++)
    A[i]=f(i); // compute values for A
  #pragma omp for
  for(int j=0; j<N/2; j++)
    B[j]=A[j]+A[j+N/2];// use the previously computed vector A
} // end of parallel section

由于所有线程都是异步工作的,因此您不知道哪些线程已经完成创建它们的 vector 部分 A。如果没有同步,线程可能会快速完成第一个 for 循环的一部分,进入第二个 for 循环并访问向量 A 的元素,而线程应该计算还在第一个循环中,还没有计算出A[i].

对应的值

这就是 openmp 编译器添加隐式屏障来同步所有线程的原因。因此,您确定所有线程都已完成所有工作,并且当第二个 for 循环开始时 A 的所有值都已计算。

但在某些情况下,不需要同步。例如,考虑以下代码:

#pragma omp parallel
{
  #pragma omp for
  for(int i=0; i<N; i++)
    A[i]=f(i); // compute values for A
  #pragma omp for
  for(int j=0; j<N/2; j++)
    B[j]=g(j);// compute values for B
} // end of parallel section

显然这两个循环是完全独立的,如果 A 被正确计算以启动第二个 for 循环并不重要。所以同步对程序的正确性没有任何帮助 添加同步屏障有两个主要缺点:

  1. 如果函数 f() 的 运行 时间非常不同,您可能有一些线程已完成工作,而其他线程仍在计算。同步将迫使之前的线程等待,而这种闲置不会适当地利用并行性。

  2. 同步很昂贵。实现屏障的一种简单方法是在到达屏障时递增一个全局计数器,并等待计数器的值等于线程数omp_get_num_threads()。为了避免线程之间的竞争,全局计数器的递增必须通过需要大量周期的原子读-修改-写来完成,并且等待计数器的正确值通常是通过自旋锁来完成的,这会浪费处理器周期。

所以有抑制隐式同步的构造,对前一个循环进行编程的最佳方法是:

#pragma omp parallel
{
  #pragma omp for nowait  // nowait suppresses implicit synchronisations. 
  for(int i=0; i<N; i++)
    A[i]=f(i); // compute values for A
  #pragma omp for
  for(int j=0; j<N/2; j++)
    B[j]=g(j);// compute values for B
} // end of parallel section

这样,一旦一个线程完成了第一个循环的工作,它将立即开始处理第二个 for 循环,并且,根据实际程序,这可能会显着减少执行时间。