OpenMP 任务看不到共享变量的更改



我启动了两个OpenMP任务,它们只是打印最初设置为1的共享变量的值。我将启动两项任务之间的变量更改为2。

我希望这两个任务都能看到变量值的变化,也就是说输出应该是2 2。然而,我总是得到1 22 1,如果变量是firstprivate,这就是我所期望的结果。

我还尝试在启动任务之前设置一个锁,并在任务注册和变量更改后取消设置。还让两个任务都等待锁定,以确保变量已经更改。结果是一样的,我没有让两个任务都看到变量(2 2)的变化值。我做错了什么?使用GCC 7.4.0。CCD_ 4返回8。

#include <iostream>
#include <omp.h>
int main()
{
omp_lock_t lock;
int i = 1;
omp_init_lock(&lock);
#pragma omp parallel default(shared) shared(i)
{
#pragma omp single
{
omp_set_lock(&lock); // set lock before any tasks are registered
#pragma omp task default(shared) shared(i)
{
omp_set_lock(&lock); // should wait until lock is unset and i is 2?
std::cout << i;
omp_unset_lock(&lock);
}
i = 2;
#pragma omp task default(shared) shared(i)
{
omp_set_lock(&lock);
std::cout << i;
omp_unset_lock(&lock);
}
omp_unset_lock(&lock); // unset lock after i is set to 2
}
}
omp_destroy_lock(&lock);
return 0;
}

编辑也许i由于某种原因没有存储在共享内存中?如果我将其更改为无法存储在寄存器中的内容,或者使其全局化,甚至只打印其地址(std::cout << &i;),则程序将按预期工作。可能是未定义的行为还是GCC问题?

首先,期望对shared依赖项进行排序只是要求竞争条件。请不要这样做——这只是一个思考练习,让你了解正在发生的事情。在任何实际的代码中,使用依赖项来强制具有依赖项的任务之间的正确数据流。

预期行为

预期的事件顺序是:

  • 输入omp single
  • 创建任务1
  • 集合i=2
  • 创建任务2
  • 到达平行区域的末端,现在正在等待任务完成
  • (可能在不同的线程/核心上)任务1执行:读取i
  • (可能在不同的线程/核心上)任务2执行:读取i
  • 任务已完成,程序终止

当前行为

然而,的任务可能会延迟执行,并且只能保证它会在当前并行区域结束之前发生,因此您不能真正将其视为顺序程序来读取。任务也可以执行未出错,即在主任务暂停的情况下立即运行。如果任务很小或者没有更多可用的线程,这通常是一个不错的选择。

来自OpenMP 4.8规范:

无错误任务

一种任务,其执行相对于其生成任务区域不延迟。也就是说,它的生成任务区域被暂停,直到未出错任务的执行完成为止。

因此,发生的事情很可能是:

  • 输入omp single
  • 创建任务1
  • 挂起父任务以执行任务1未出错
  • 集合i=2
  • 挂起父任务以执行任务2未出错
  • 程序终止

如何修复

相反,您应该在任务所需的数据准备就绪时运行任务:

int main()
{
int i = 1;
#pragma omp parallel
#pragma omp single
{
#pragma omp task depend(in:i)
{
std::cout << 'a' << i;
}
#pragma omp task depend(out:i)
i = 2;
#pragma omp task depend(in:i)
{
std::cout << 'b' << i;
}
#pragma omp task depend(in:i)
{
std::cout << 'c' << i;
}
}
return 0;
}

这应该始终返回a1c2b2a1b2c2。请注意,我说应该,因为写入stdout也不是真正的原子,所以理论上我不能排除偶尔出现的abc122或类似的情况。

任务3和4仅在任务2完成后运行,并确保数据被正确转发。

如何而不是来修复它

创建将挂起子任务的锁,恢复父任务,只会使事情变得复杂。

事件序列变为:

  • 输入omp single
  • 生成任务时获取锁定
  • 创建任务1
  • 挂起父任务以执行任务1未出错
  • 挂起任务1等待锁定
  • 恢复生成任务
  • 集合i=2
  • 挂起父任务以执行任务2未出错
  • 挂起任务2等待锁定
  • 恢复生成任务
  • 生成任务时释放锁定
  • 恢复任务1,获取锁定,打印1,释放锁定
  • 恢复任务2,获取锁定,打印2,释放锁定
  • 程序终止

锁不会影响i,它们只是挂起子任务,直到生成任务结束。某种形式的内存屏障/刷新可能会解决这个问题,您还需要停止编译器根据锁的获取和释放来重新排序对i的访问。实现这一点的最简单方法是使i成为原子int:

(请不要使用此代码)

int main()
{
omp_lock_t lock;
omp_init_lock(&lock);
std::atomic<int> i(1);
#pragma omp parallel shared(i)
#pragma omp single
{
omp_set_lock(&lock);
#pragma omp task shared(i)
{
// enter task, then suspend until i = 2
omp_set_lock(&lock);
std::cout << i;
omp_unset_lock(&lock);
}
i = 2;
#pragma omp task shared(i)
{
// enter task, then suspend until i = 2
omp_set_lock(&lock);
std::cout << i;
omp_unset_lock(&lock);
}
// unset lock after i is set to 2 and child tasks are created
// child tasks are possibly started and suspended at this point
omp_unset_lock(&lock);
}
omp_destroy_lock(&lock);
std::cout << std::endl;
return 0;
}

然而,在任务并行程序上使用线程并行结构是错误的方法期望对shared依赖项进行排序只是要求竞争条件此外,您正在创建任务以立即挂起它们,这毫无意义。

使用volatile int i,让我们看一下以下任务的汇编(来自带有-S -fverbose-asm的gcc输出)(带有###的行是我的注释):

#pragma omp task shared(i)
{
// enter task, then suspend until i = 2
omp_set_lock(&lock);
__asm__ volatile("mfence":::"memory");
std::cout << i;
omp_unset_lock(&lock);
}
.LFB2346:
.cfi_startproc
.cfi_personality 0x3,__gxx_personality_v0
.cfi_lsda 0x3,.LLSDA2346
pushq   %rbp    #
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq    %rsp, %rbp  #,
.cfi_def_cfa_register 6
subq    $32, %rsp   #,
### get "omp_data_i", a struct containing _the value of i_ and the lock id
movq    %rdi, -24(%rbp) # .omp_data_i, .omp_data_i
### get i, store it on the stack at -4
# lock+volatile.cc:15:         #pragma omp task shared(i)
movq    -24(%rbp), %rax # .omp_data_i, tmp86
movl    8(%rax), %eax   # .omp_data_i_2(D)->i, i.6_3
movl    %eax, -4(%rbp)  # i.6_3, i
### get the lock id and call omp_set_lock
# lock+volatile.cc:18:             omp_set_lock(&lock);
movq    -24(%rbp), %rax # .omp_data_i, tmp87
movq    (%rax), %rax    # .omp_data_i_2(D)->lock, _5
movq    %rax, %rdi  # _5,
call    omp_set_lock    #
### our manually written assembly
# lock+volatile.cc:20:             __asm__ volatile("mfence":::"memory");
#APP
# 20 "lock+volatile.cc" 1
mfence  
# 0 "" 2
### get i from the stack and call cout
# lock+volatile.cc:21:             std::cout << i;
#NO_APP
movl    -4(%rbp), %eax  # i, i.0_9
movl    %eax, %esi  # i.0_9,
movl    $_ZSt4cout, %edi    #,
call    _ZNSolsEi   #
### get the lock and call unset_lock
# lock+volatile.cc:22:             omp_unset_lock(&lock);
movq    -24(%rbp), %rax # .omp_data_i, tmp88
movq    (%rax), %rax    # .omp_data_i_2(D)->lock, _11
movq    %rax, %rdi  # _11,
call    omp_unset_lock  #

有了int i(非易失性),现在让我们看看这个任务的汇编:

#pragma omp task shared(i)
{
// enter task, then suspend until i = 2
omp_set_lock(&lock);
std::cout << __atomic_load_n(&i, __ATOMIC_RELAXED);
omp_unset_lock(&lock);
}
.LFB2346:
.cfi_startproc
.cfi_personality 0x3,__gxx_personality_v0
.cfi_lsda 0x3,.LLSDA2346
pushq   %rbp    #
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq    %rsp, %rbp  #,
.cfi_def_cfa_register 6
subq    $16, %rsp   #,
### get "omp_data_i", a struct containing _the address of i_ and the lock id
movq    %rdi, -8(%rbp)  # .omp_data_i, .omp_data_i
### get the lock id and call omp_set_lock
# lock+volatile.cc:18:             omp_set_lock(&lock);
movq    -8(%rbp), %rax  # .omp_data_i, tmp87
movq    (%rax), %rax    # .omp_data_i_2(D)->lock, _3
movq    %rax, %rdi  # _3,
call    omp_set_lock    #
### get i and call cout
# lock+volatile.cc:19:             std::cout << __atomic_load_n(&i, __ATOMIC_RELAXED);
movq    -8(%rbp), %rax  # .omp_data_i, tmp88
movq    8(%rax), %rax   # .omp_data_i_2(D)->i, _6
movl    (%rax), %eax    #* _6, _9
movl    %eax, %esi  # _10,
movl    $_ZSt4cout, %edi    #,
call    _ZNSolsEi   #
### get the lock id and call unset_lock
# lock+volatile.cc:20:             omp_unset_lock(&lock);
movq    -8(%rbp), %rax  # .omp_data_i, tmp89
movq    (%rax), %rax    # .omp_data_i_2(D)->lock, _12
movq    %rax, %rdi  # _12,
call    omp_unset_lock  #

如您所见,在第一种情况下,在omp_set_lock调用之前执行在寄存器中获取i的值。只有使用atomic(即使具有宽松的一致性),我才设法将其移动到"预期"的位置,大概是因为这样访问就无法相对于锁进行重新排序。

最新更新