我启动了两个OpenMP任务,它们只是打印最初设置为1的共享变量的值。我将启动两项任务之间的变量更改为2。
我希望这两个任务都能看到变量值的变化,也就是说输出应该是2 2
。然而,我总是得到1 2
或2 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;
}
这应该始终返回a1c2b2
或a1b2c2
。请注意,我说应该,因为写入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(即使具有宽松的一致性),我才设法将其移动到"预期"的位置,大概是因为这样访问就无法相对于锁进行重新排序。