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

时间:2019-11-24 10:51:04

标签: c++ gcc task openmp memory-model

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

我希望两个任务都能看到变量的更改后的值,即输出应为2 2。但是,我总是得到1 22 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问题?

1 个答案:

答案 0 :(得分:3)

首先,期望对shared依赖项的排序只是在询问竞争条件。请不要这样做-这只是一种思想练习,因此您可以了解正在发生的事情。在任何实际代码中,都应使用依赖项,以在具有依赖项的任务之间强制适当的数据流。

预期的行为

预期的事件顺序是:

  • 输入omp单
  • 创建任务1
  • 设置i = 2
  • 创建任务2
  • 到达并行区域的尽头,现在正在等待任务完成
  • (可能在其他线程/内核上)任务1执行:读取i
  • (可能在不同的线程/内核上)执行任务2:读取i
  • 任务完成,程序终止

当前行为

但是任务可能会延迟执行,并且只能保证它会在当前并行区域结束之前发生,因此您无法真正将其读为顺序执行程序。任务可能还具有 undeferred 执行,该执行将在主要任务挂起的情况下立即运行。如果任务很小或没有更多可用线程,通常这是一个不错的选择。

根据OpenMP 4.8规范:

  

未延期的任务

     

相对于其生成任务区域而言,不推迟执行的任务。   也就是说,将其生成任务区域挂起,直到完成未延迟任务的执行为止。

因此,最有可能发生的事情是

  • 输入omp单
  • 创建任务1
  • 暂停父任务以执行任务1 undeferred
  • 设置i = 2
  • 暂停父任务以执行任务2 undeferred
  • 程序终止

如何修复

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

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单
  • 获取生成任务的锁
  • 创建任务1
  • 暂停父任务以执行任务1 undeferred
  • 挂起任务1等待锁定
  • 还原生成任务
  • 设置i = 2
  • 暂停父任务以执行任务2 undeferred
  • 挂起任务2,等待锁定
  • 还原生成任务
  • 释放锁定以生成任务
  • 还原任务1,获取锁定,打印1,释放锁定
  • 还原任务2,获取锁定,打印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的值的操作。我只能使用原子(即使具有宽松的一致性)也设法将其移动到“期望的”位置,大概是因为这样就无法相对于锁对访问进行重新排序。