一个静态随机数生成器的线程安全性

5

我有很多线程,每个线程都需要一个安全的随机数。由于在我的真实程序中会反复产生和加入线程,我不想每次进入调用相同函数的新并行区域时都创建random_devicemt19937,所以我将它们设为静态变量:

#include <iostream>
#include <random>
#include <omp.h>

void test(void) {
    static std::random_device rd;
    static std::mt19937 rng(rd());
    static std::uniform_int_distribution<int> uni(1, 1000);

    int x = uni(rng);
#   pragma omp critical
    std::cout << "thread " << omp_get_thread_num() << " | x = " << x << std::endl;
}

int main() {
#   pragma omp parallel num_threads(4)
    test();
}

由于发生了“错误C3057:当前不支持'threadprivate'符号的动态初始化”,我无法将它们作为threadprivate。有一些资料表明random_devicemt19937是线程安全的,但我没有找到任何可以证明它的文档。

  1. 这个随机化是否是线程安全的?
  2. 如果不是,哪些静态对象可以保留为静态以保持线程安全?

如果有帮助的话,您可以将它们设置为 static thread_local,但不确定是否与 OpenMP 兼容。如果 rd 仅用于初始化,请考虑使用 rng(std::random_device{}()) - Quimby
4
是的,mt19937uni都有内部状态,并修改它们以生成随机数。因为它们会自我修改,如果在多个线程之间共享,需要进行保护。 - NathanOliver
2
一些来源表示random_device和mt19937是线程安全的,但我还没有找到任何证明的文档。这些“文档”就是C++11标准。本地的“静态”变量保证以线程安全的方式访问,否则Meyers单例将无法工作。问题在于uni(rng)。 - PaulMcKenzie
2
@paleonix 那不是正确的语言。(即使那种语言的解决方案可以适用于这种语言)。 - cigien
@Kaiyakha 因为 {} 创建了一个临时对象,而 () 调用了它的调用运算符。 - Quimby
显示剩余12条评论
2个回答

1

这里有一种不同的方法。我保留了全局种子值,以便random_device仅被使用一次。由于使用它可能非常缓慢,我认为只在必要时尽可能少地使用它是明智的。

相反,我们每个线程和每个使用递增种子值。这样我们就避免了生日悖论,并将线程本地状态最小化为一个整数。

#include <omp.h>

#include <algorithm>
#include <array>
#include <random>


using seed_type = std::array<std::mt19937::result_type, std::mt19937::state_size>;


namespace {

  seed_type init_seed()
  {
    seed_type rtrn;
    std::random_device rdev;
    std::generate(rtrn.begin(), rtrn.end(), std::ref(rdev));
    return rtrn;
  }
  
}
/**
 * Provides a process-global random seeding value
 *
 * Thread-safe (assuming the C++ compiler if standard-conforming.
 * Seed is initialized on first call
 */
seed_type global_seed()
{
  static seed_type rtrn = init_seed();
  return rtrn;
}
/**
 * Creates a new random number generator
 *
 * Operation is thread-safe, Each thread will get its own RNG with a different
 * seed. Repeated calls within a thread will create different RNGs, too.
 */
std::mt19937 make_rng()
{
  static std::mt19937::result_type sequence_number = 0;
# pragma omp threadprivate(sequence_number)
  seed_type seed = global_seed();
  static_assert(seed.size() >= 3);
  seed[0] += sequence_number++;
  seed[1] += static_cast<std::mt19937::result_type>(omp_get_thread_num());
  seed[2] += static_cast<std::mt19937::result_type>(omp_get_level());
  std::seed_seq sseq(seed.begin(), seed.end());
  return std::mt19937(sseq);
}

请参考这个链接: 如何使用openMP使此代码线程安全?蒙特卡洛二维积分 对于仅增加种子值的方法,请参考此链接:https://www.johndcook.com/blog/2016/01/29/random-number-generator-seed-mistakes/

1
如果随机数的质量很重要,我建议阅读梅丽莎·奥尼尔的PCG博客 - paleonix
你可能还需要考虑一个旨在并行使用的随机数生成器。例如,可以参考《Parallel Random Numbers: As Easy as 1, 2, 3》(http://www.thesalmons.org/john/random123/papers/random123sc11.pdf),该方法现在已经被许多数学库实现。 - Jim Cownie

0

我认为threadprivate仍然是正确的方法,您可以通过稍后进行并行赋值来避免初始化问题。

static random_device rd;
static mt19937 rng;
#pragma omp threadprivate(rd)
#pragma omp threadprivate(rng)

int main() {

#pragma omp parallel
  rng = mt19937(rd());

#pragma omp parallel
  {
    stringstream res;
    uniform_int_distribution<int> uni(1, 100);
    res << "Thread " << omp_get_thread_num() << ": " << uni(rng) << "\n";
    cout << res.str();
  }

  return 0;
}

顺便提一下,注意使用stringstream:OpenMP倾向于在<<运算符处拆分输出行。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接