【问题标题】:OpenMP: Is a barrier inside conditional code valid?OpenMP:条件代码中的障碍是否有效?
【发布时间】:2021-12-09 08:16:13
【问题描述】:

OpenMP Specification 中,对屏障构造提出了以下限制:(参见第 259 页,第 30-31 行):

每个障碍区域必须由团队中的所有线程或 根本没有,除非最里面的人要求取消 封闭平行区域。

为了完整起见,OpenMP 规范对 区域 的定义如下(参见第 5 页,第 9 行以下):

地区

在特定实例中遇到的所有代码 执行给定的构造、结构化的块序列或 OpenMP 库例程。区域包括被调用例程中的任何代码 以及任何实现代码。 [...]

我想出了一个非常简单的例子,我问自己它是否有效,因为障碍被放置在 if 条件中(并不是每个障碍都被每个线程“看到”)。尽管如此,每个线程的屏障数量是相同的,并且使用两个编译器进行的实验表明代码可以按预期工作。

#include <stdio.h>
#include <unistd.h>
#include <stdarg.h>
#include <sys/time.h>
#include "omp.h"

double zerotime;

double gettime(void) {
 struct timeval t;
 gettimeofday(&t, NULL);
 return t.tv_sec + t.tv_usec * 1e-6;
}

void print(const char *format, ...) {
  va_list args;
  va_start (args, format);
  #pragma omp critical
  {
    fprintf(stdout, "Time = %1.1lfs ", gettime() - zerotime);
    vfprintf (stdout, format, args);
  }
  va_end (args);
}

void barrier_test_1(void) {
  for (int i = 0; i < 5; i++) {
    if (omp_get_thread_num() % 2 == 0) {
      print("Path A: Thread %d waiting\n", omp_get_thread_num());
      #pragma omp barrier
    } else {
      print("Path B: Thread %d waiting\n", omp_get_thread_num());
      sleep(1);
      #pragma omp barrier
    }
  }
}

int main() {
zerotime = gettime();
#pragma omp parallel
{
  barrier_test_1();
}
return 0;
}

对于四个线程,我得到以下输出:

Time = 0.0s Path B: Thread 1 waiting
Time = 0.0s Path B: Thread 3 waiting
Time = 0.0s Path A: Thread 0 waiting
Time = 0.0s Path A: Thread 2 waiting
Time = 1.0s Path B: Thread 1 waiting
Time = 1.0s Path B: Thread 3 waiting
Time = 1.0s Path A: Thread 2 waiting
Time = 1.0s Path A: Thread 0 waiting
Time = 2.0s Path B: Thread 1 waiting
Time = 2.0s Path B: Thread 3 waiting
Time = 2.0s Path A: Thread 0 waiting
Time = 2.0s Path A: Thread 2 waiting
...

这表明所有线程都很好地等待缓慢的 Path B 操作并配对,即使它们没有放在同一个分支中。 但是,我仍然对规范感到困惑,我的代码是否有效。 对比一下,例如与CUDA 相关的__syncthreads() 例程给出以下声明:

__syncthreads() 在条件代码中是允许的,但前提是条件在整个线程块中的计算结果相同, 否则代码执行可能会挂起或产生意外 副作用。

因此,在 CUDA 中,上面以 __syncthreads() 编写的此类代码将无效,因为条件 omp_get_thread_num() % 2 == 0 的计算结果因线程而异。

后续问题:

虽然我对上面的代码不符合规范的结论很满意,但可以对代码稍作修改如下,其中barrier_test_1()barrier_test_2() 替换:

void call_barrier(void) {
  #pragma omp barrier
}

void barrier_test_2(void) {
  for (int i = 0; i < 5; i++) {
    if (omp_get_thread_num() % 2 == 0) {
      print("Path A: Thread %d waiting\n", omp_get_thread_num());
      call_barrier();
    } else {
      print("Path B: Thread %d waiting\n", omp_get_thread_num());
      sleep(1);
      call_barrier();
    }
  }
}

我们认识到,我们在代码中只放置了一个屏障,团队中的所有线程都会访问这个屏障。虽然上述代码在 CUDA 案例中仍然无效,但我仍然不确定 OpenMP。我认为这归结为真正构成障碍区域的问题,它只是代码中的一行,还是在后续障碍之间遍历的所有代码?这也是我在规范中查找region定义的原因。更准确地说,据我所知,没有code encountered during a specific instance of the execution of &lt;the barrier construct&gt;,这是由于规范中关于独立指令的声明(p.45,第 3+5 行)

独立指令是可执行指令,没有 关联的用户代码。

独立指令没有任何关联的可执行用户 代码。

自(第 258 页第 9 行)

屏障结构是一个独立的指令。

也许规范的以下部分也很有趣(第 259 页,第 32-33 行):

遇到的工作共享区域和障碍区域的顺序 团队中的每个线程都必须相同。

初步结论:

我们可以像上面一样将屏障包装到单个函数中,并通过调用包装函数来替换所有屏障,这会导致:

  1. 所有线程要么继续执行用户代码,要么在屏障处等待
  2. 如果我们只通过一部分线程调用包装器,这将导致死锁,但不会导致未定义的行为
  3. 在对包装器的调用之间,线程中遇到的障碍数相同
  4. 基本上这意味着,我们可以通过使用此类包装器安全地同步和切断不同的执行路径

我说的对吗?

【问题讨论】:

  • 我猜它是无效的,纯粹是偶然的。规范要求所有线程访问每个屏障
  • 假设支持:运行时如何知道线程何时可以到达屏障并且必须等待?实际上,C++ 程序的静态分析通常是不可判定的(例如,停止问题)。线程的数量是动态的,并且线程(大部分)不像 CUDA 中那样以锁步方式执行(参见 SMT 模型)。
  • @JérômeRichard 我不是在问原则上如何做,问题是规范是否允许我编写这样的代码,以及它的行为是否定义良好。当然,思考如何实现这样的功能是一个有趣但完全不同的问题。
  • @tstanisl 请查看后续问题,该问题分别解决了这个微妙的问题。

标签: c multithreading openmp


【解决方案1】:

在 OpenMP 规范中,对 a 屏障构造:(参见第 259 页,第 30-31 行):

每个障碍区域必须由团队中的所有线程或 根本没有,除非最里面的人要求取消 封闭平行区域。

这个描述有点问题,因为barrier 是一个独立 指令。这意味着它除了指令本身之外没有关联代码,因此不存在“屏障区域”之类的东西。

尽管如此,我认为意图很明确,无论是从措辞本身还是从屏障实现的常规行为来看:没有任何取消,如果执行包含给定 barrier 构造的最内部并行区域的团队中的任何线程达到那个屏障,则团队中的所有线程都必须达到相同的 barrier 构造。不同的屏障构造代表不同的屏障,每个屏障都要求所有线程在任何继续通过之前到达。

但是,我仍然对规范感到困惑,我的代码是否有效。

我看到您的测试代码的行为表明这两个障碍被视为一个障碍。然而,这与解释规范无关,因为您的代码确实不满足您询问的要求。在这种情况下,规范不要求程序以任何特定方式失败,但它当然也不需要您观察到的行为。您可能会发现,使用不同版本的编译器或不同的 OpenMP 实现时,程序的行为会有所不同。编译器有权假定您的 OpenMP 代码符合 OpenMP 规范。

当然,在您的特定示例中,解决方案是将不同条件分支中的两个 barrier 构造替换为紧跟在 else 块之后的一个构造。

【讨论】:

  • 感谢您的回答。我也有同样的想法,但我仍然有一种轻微的感觉,规范的措辞并没有真正的帮助。我也喜欢你提出的解决方案。当然,在上面的简单问题中,解决方案在某种程度上是微不足道的。但是在一个更复杂的问题中,是否有另一种方法可以在不使用障碍的情况下同步不同的线程(或同步发生在代码中同一点的限制)?
  • @questioner,如果你有多个通过并行区域的执行路径,并且你想要一个跨越这些执行路径的屏障,那么我认为你可以用 OpenMP 做的最好的事情就是让所有路径汇合在一起,将障碍放在那里,然后将它们分开。有几种方法可以做到这一点。然而,事实上,这种需求是非常不寻常的。
  • 另外,@questioner,如果您有复杂的同步要求,那么可能是 OpenMP 不能很好地支持它们。灵活性的限制是您为 OpenMP 编程接口的统一性和简单性付出的代价之一。
  • 当然有限制,但坚持使用 OpenMP 也很好,因为如果我去,例如对于 MPI,同步通常更容易,但共享数据变得很痛苦。
  • 因此得出结论:对屏障构造的限制与 CUDA 的情况相同吗?只要条件对所有线程的评估相同,我们就可以在 if 语句中放置障碍。同样,只要线程组之间的所有循环迭代相同,就可以在 for 循环中放置障碍以进行同步?
猜你喜欢
  • 1970-01-01
  • 2016-07-19
  • 2017-04-26
  • 2022-11-10
  • 2017-07-01
  • 1970-01-01
  • 1970-01-01
  • 2018-04-21
  • 2011-10-16
相关资源
最近更新 更多