任务完成后复制 OpenMP 整数

Cow*_*Man 1 c++ multithreading gcc mutex openmp

我不知道这是否在任何地方都有记录,如果有,我希望引用它,但是我在使用 OpenMP 时发现了一些意外的行为。我下面有一个简单的程序来说明这个问题。在这里,我将以点的形式讲述我期望该程序执行的操作:

  • 我想要 2 个线程
  • 他们都共享一个整数
  • 第一个线程递增整数
  • 第二个线程读取整数
  • 递增一次后,外部进程必须告诉第一个线程继续递增(通过互斥锁)
  • 第二个线程负责解锁该互斥体

正如您将看到的,线程之间共享的计数器对于第二个线程没有正确更改。但是,如果我将计数器转换为整数引用,我会得到预期的结果。这是一个简单的代码示例:

#include <mutex>
#include <thread>
#include <chrono>
#include <iostream>
#include <omp.h>

using namespace std;
using std::this_thread::sleep_for;
using std::chrono::milliseconds;

const int sleep_amount = 2000;

int main() {

  int counter = 0; // if I comment this and uncomment the 2 lines below, I get the expected results
  /* int c = 0; */
  /* int &counter = c; */

  omp_lock_t mut;
  omp_init_lock(&mut);
  int counter_1, counter_2;

#pragma omp parallel
#pragma omp single
  {
#pragma omp task default(shared)
// The first task just increments the counter 3 times
    {
      while (counter < 3) {
        omp_set_lock(&mut);
        counter += 1;
        cout << "increasing: " << counter << endl;
      }
    }
#pragma omp task default(shared)
    {
      sleep_for(milliseconds(sleep_amount));
      // While sleeping, counter is increased to 1 in the first task
      counter_1 = counter;
      cout << "counter_1: " << counter << endl;

      omp_unset_lock(&mut);
      sleep_for(milliseconds(sleep_amount));
      // While sleeping, counter is increased to 2 in the first task
      counter_2 = counter;
      cout << "counter_2: " << counter << endl;
      omp_unset_lock(&mut);
      // Release one last time to increment the counter to 3
    }
  }
  omp_destroy_lock(&mut);

  cout << "expected: 1, actual: " << counter_1 << endl;
  cout << "expected: 2, actual: " << counter_2 << endl;
  cout << "expected: 3, actual: " << counter << endl;
}
Run Code Online (Sandbox Code Playgroud)

这是我的输出:

increasing: 1
counter_1: 0
increasing: 2
counter_2: 0
increasing: 3
expected: 1, actual: 0
expected: 2, actual: 0
expected: 3, actual: 3
Run Code Online (Sandbox Code Playgroud)

海湾合作委员会版本:9.4.0

其他发现:

  • 如果我使用 OpenMP“部分”而不是“任务”,我也会得到预期的结果。问题似乎出在“任务”上
  • 如果我使用 posix 信号量,这个问题仍然存在。

Jér*_*ard 5

不允许从另一个线程解锁互斥锁。这样做会导致未定义的行为。在这种情况下,一般的解决方案是使用信号量。等待条件也有帮助(关于现实世界的用例)。引用OpenMP 文档(请注意,几乎所有互斥体实现(包括 pthreads)都共享此约束):

访问未处于锁定状态或不属于包含通过任一例程进行的调用的任务所拥有的锁的程序是不合格的。
通过任一例程访问未处于未初始化状态的锁的程序是不合格的。

而且,这两个任务可以在同一个线程上执行,也可以在不同线程上执行。您不应该对它们的调度做出任何假设,除非您告诉 OpenMP 使用依赖项来这样做。在这里,运行时完全符合串行执行任务的要求。您需要使用OpenMP 部分,以便多个线程执行不同的部分。此外,在任务中使用锁通常被认为是一种不好的做法,因为运行时调度程序不知道它们。

最后,在这种情况下您不需要锁:原子操作就足够了。幸运的是,OpenMP 支持原子操作(以及 C++)。


补充笔记

请注意,由于内存屏障,锁保证了多个线程中内存访问的一致性。事实上,互斥体上的解锁操作会导致释放内存屏障,从而使写入对其他线程可见。来自另一个线程的锁会执行获取内存屏障,强制在锁之后完成读取。当未正确使用锁定/解锁时,完成内存访问的方式不再安全,这会导致某些变量无法从其他线程更新。更一般地说,这也往往会产生竞争条件。因此,简单地说,不要这样做。