Openmp 多线程代码在使用多个线程时给出不同的答案

Posted

技术标签:

【中文标题】Openmp 多线程代码在使用多个线程时给出不同的答案【英文标题】:Openmp multithreaded code giving different answer when using multiple threads 【发布时间】:2020-10-21 14:38:46 【问题描述】:

我有以下带有openmp 多线程的蒙特卡罗代码

int main()

  std::uniform_int_distribution<int> dir(0, 1);
  std::uniform_int_distribution<int> xyz(0, 2);
  std::uniform_real_distribution<double> angle(0,360);
  mt19937 gen0;

  auto M = 20;
  long double sum = 0;
  auto num_trials = 10000;
  // omp_set_num_threads(12);

  #pragma omp parallel
  
    double loc_sum = 0.0;
    #pragma omp for
    for(int i = 0; i < num_trials; ++i)
    
      double x = 0;
      double y = 0;
      double z = 0;
      double r = 0; 
      auto N = 0;
      
      while(r < M)
      
        auto d = dir(gen);
        auto p = xyz(gen);
        if(p == 0)
        
          x += (d == 1) ? -1 : 1;
        
        else if(p == 1)
        
          y += (d == 1) ? -1 : 1;
        
        else
        
          z += (d == 1) ? -1 : 1;
        

        r = std::sqrt(x * x + y * y + z * z);
        ++N;
      

      loc_sum += N;
    

    #pragma omp critical
      sum += loc_sum;
  

变量sum 在串行和多线程中执行是完全不同的。由于对随机均匀分布的调用,我预计会略有不同,但我观察到的差异太显着了,不可能是由于随机性,我怀疑我的多线程代码中存在错误。

此代码中是否存在影响sum 的竞争条件或数据竞争?

【问题讨论】:

我很确定你不能在没有锁的情况下调用这些生成器(dirxyz)。您还在使用 PRNG (gen) 而不锁定。如果您使用#pragma omp critical 标记生成dp 的行,代码是否有效? @Darhuuk 啊,我认为你是对的。用关键部分包裹d,p,它可以工作。我原以为生成器会以原子方式产生值。实际上,我不想在d,p 周围放置一个关键部分。这里最好的解决方案是在并行区域内创建生成器吗? 是的,在这种情况下,您最好的选择是创建生成器的副本。那些应该是相当轻量级的。您可能不想复制 PRNG 对象,因为那时您将在每个线程中拥有完全相同的状态。所以我会用proper initialization在for循环中创建它。 以正确的答案扩展了我的 cmets。 【参考方案1】:

问题是您调用生成器(dirxyz)时没有锁定它们。您还使用了没有锁定的 PRNG (gen)。

这些调用都不是原子的,因为默认情况下实现它们会使单线程代码比需要的慢。

使用#pragma omp critical 标记生成dp 的行应该可以解决问题。

如果您不想要临界区,则需要在每个线程中分别使用 dirxyzgen 对象。可以简单地复制生成器(dirxyz)。 PRNG (gen) 应该在每个线程中正确初始化,否则您最终会得到每个线程中具有完全相同状态的 PNG。例如:

std::random_device rd; /* Outside the parallel section. */

// Code below once per thread.
/* Initialization of the PRNG calls std::random_device::operator() which 
 * needs a lock around it when called in parallel. */
std::mt199937 gen;    
#pragma omp critical

  gen.seed(rd());


// for-loop starts here.

【讨论】:

rd() 调用中是否存在竞争条件? 除非明确提到有问题的函数是原子的,否则我会假设会有竞争条件。标准库中的函数极不可能是原子的,因为这会使它比不使其成为原子的要慢。 IE。通过使其具有原子性,您可以惩罚从单个线程中使用该函数的每个人。相反,如果您编写非原子函数,无论如何都可以通过在调用周围加锁来使它们成为原子函数。现在,您只需在真正需要时支付锁定/原子调用的代价。 此外,围绕生成器播种的关键部分不应对程序的运行时产生任何明显影响,因为它每个线程只运行一次。

以上是关于Openmp 多线程代码在使用多个线程时给出不同的答案的主要内容,如果未能解决你的问题,请参考以下文章

从多个线程读取数组时要注意啥?

在不同的线程下运行相同的代码有啥意义 - openMP?

NodeJS:具有多线程的本机 C++ 模块(openmp)

在 Openmp (C++) 中销毁线程

我可以将多个线程分配给 OpenMP 中的代码段吗?

在C++中使用openmp进行多线程编程