【发布时间】: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 <the barrier construct>,这是由于规范中关于独立指令的声明(p.45,第 3+5 行)
独立指令是可执行指令,没有 关联的用户代码。
和
独立指令没有任何关联的可执行用户 代码。
自(第 258 页第 9 行)
屏障结构是一个独立的指令。
也许规范的以下部分也很有趣(第 259 页,第 32-33 行):
遇到的工作共享区域和障碍区域的顺序 团队中的每个线程都必须相同。
初步结论:
我们可以像上面一样将屏障包装到单个函数中,并通过调用包装函数来替换所有屏障,这会导致:
- 所有线程要么继续执行用户代码,要么在屏障处等待
- 如果我们只通过一部分线程调用包装器,这将导致死锁,但不会导致未定义的行为
- 在对包装器的调用之间,线程中遇到的障碍数相同
- 基本上这意味着,我们可以通过使用此类包装器安全地同步和切断不同的执行路径
我说的对吗?
【问题讨论】:
-
我猜它是无效的,纯粹是偶然的。规范要求所有线程访问每个屏障
-
假设支持:运行时如何知道线程何时可以到达屏障并且必须等待?实际上,C++ 程序的静态分析通常是不可判定的(例如,停止问题)。线程的数量是动态的,并且线程(大部分)不像 CUDA 中那样以锁步方式执行(参见 SMT 模型)。
-
@JérômeRichard 我不是在问原则上如何做,问题是规范是否允许我编写这样的代码,以及它的行为是否定义良好。当然,思考如何实现这样的功能是一个有趣但完全不同的问题。
-
@tstanisl 请查看后续问题,该问题分别解决了这个微妙的问题。
标签: c multithreading openmp