我启动了两个OpenMP任务,这些任务只是打印最初设置为1的共享变量的值。我将启动两个任务的变量更改为2。
我希望两个任务都能看到变量的更改后的值,即输出应为2 2
。但是,我总是得到1 2
或2 1
,如果变量是firstprivate,这就是我期望的结果。
我还尝试在启动任务之前设置锁定,并在注册任务和更改变量后取消设置锁定。还使两个任务都等待锁以确保变量已更改。结果是一样的,我不能同时执行两个任务来查看变量的值(2 2)。我怎么了?使用GCC 7.4.0。 omp_get_num_threads
返回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问题?
答案 0 :(得分:3)
首先,期望对shared
依赖项的排序只是在询问竞争条件。请不要这样做-这只是一种思想练习,因此您可以了解正在发生的事情。在任何实际代码中,都应使用依赖项,以在具有依赖项的任务之间强制适当的数据流。
预期的事件顺序是:
但是任务可能会延迟执行,并且只能保证它会在当前并行区域结束之前发生,因此您无法真正将其读为顺序执行程序。任务可能还具有 undeferred 执行,该执行将在主要任务挂起的情况下立即运行。如果任务很小或没有更多可用线程,通常这是一个不错的选择。
根据OpenMP 4.8规范:
未延期的任务
相对于其生成任务区域而言,不推迟执行的任务。 也就是说,将其生成任务区域挂起,直到完成未延迟任务的执行为止。
因此,最有可能发生的事情是
相反,您应该在任务所需的数据准备就绪时运行它们:
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;
}
这应始终返回a1c2b2
或a1b2c2
。请注意,我之所以说应该,是因为写入stdout也不是原子操作,因此从理论上讲,我不能排除偶发的abc122
之类的东西。
任务3和4仅在任务2完成后才运行,并确保正确转发数据。
创建将暂停子任务,还原父任务的锁只会使事情复杂化。
事件的顺序变为:
锁不影响i
,它们只是挂起子任务,直到生成任务结束为止。某种形式的内存屏障/刷新可能会解决该问题,并且您还需要停止编译器就获取和释放锁重新排序对i
的访问。实现此目的的最简单方法是使i
成为原子整数:
(请不要使用此代码)
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
,让我们看一下以下任务的程序集(来自gcc输出的-S -fverbose-asm
)(带有###
的行是我的评论):
#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 #
如您所见,在第一种情况下,在调用i
之前执行了在寄存器中获取omp_set_lock
的值的操作。我只能使用原子(即使具有宽松的一致性)也设法将其移动到“期望的”位置,大概是因为这样就无法相对于锁对访问进行重新排序。