多线程的随机数

Random numbers for multiple threads

本文关键字:随机数 多线程      更新时间:2023-10-16

问题

我打算为Linux编写一个C++11应用程序,它基于大约一百万个伪随机32位数字进行一些数值模拟(而不是密码学)。为了加快速度,我想使用桌面CPU的所有内核在并行线程中执行模拟。我想使用boost提供的Mersenne Twister mt19937作为PRNG,出于性能原因,我想每个线程应该有一个这样的PRNG。现在我不确定如何对它们进行种子处理,以避免在多个线程中生成相同的随机数子序列。

备选方案

以下是我迄今为止想到的替代方案:

  1. 独立于/dev/urandom为每个线程设置PRNG种子。

    我有点担心系统熵池耗尽的情况,因为我不知道系统内部的PRNG是如何运行的。会不会因为/dev/urandom本身使用的是Mersenne Twister,所以我意外地得到了连续的种子,这些种子准确地识别了Mersene Twister的连续状态?可能与我对下一点的担忧密切相关。

  2. /dev/urandom中播种一个PRNG,并从第一个PRNG中播种其他PRNG。

    基本上也是同样的问题:使用一个PRNG来播种另一个使用相同算法的PRNG是好是坏?或者换句话说,从mt19937读取625个32位整数是否直接对应于mt19937生成器在生成过程中的任何时刻的内部状态?

  3. 使用非梅森信息从第一个种子开始播种其他种子。

    由于使用相同的算法生成随机数和生成初始种子感觉可能是个坏主意,我考虑引入一些不依赖于Mersenne Twister算法的元素。例如,我可以将线程id异或到初始种子向量的每个元素中。这会让事情变得更好吗?

  4. 在线程之间共享一个PRNG。

    这将确保只有一个序列,具有Mersenne Twister的所有已知和理想特性。但控制访问该生成器所需的锁定开销确实让我有些担心。由于我没有发现相反的证据,我认为我作为图书馆用户将负责防止同时访问PRNG。

  5. 预生成所有随机数。

    这将使一个线程提前生成所有所需的1M随机数,供不同线程稍后使用。与整个应用程序相比,4M的内存需求将很小。在这种方法中,我最担心的是随机数的生成本身不是并发的。整个方法的规模也不太大。

问题

你会建议以下哪种方法,为什么?或者你有不同的建议吗?

你知道我的哪些担忧是合理的吗?哪些只是因为我对事情的实际运作缺乏洞察力?

我会选择#1,从urandom为每个prng播种。这确保了状态是完全独立的(只要种子数据是独立的)。通常情况下,除非有许多线程,否则会有大量的熵可用。此外,根据/dev/urandom使用的算法,你几乎可以肯定不需要担心它

因此,您可以使用以下内容来创建每个prng:

#include <random>
std::mt19937 get_prng() {
    std::random_device r;
    std::seed_seq seed{r(), r(), r(), r(), r(), r(), r(), r()};
    return std::mt19937(seed);
}

您应该验证std::random_device的实现是从您的配置下的/dev/urandom中提取的。如果它默认使用/dev/urandom,那么如果您想使用/dev/random,通常可以说std::random_device("/dev/random")

您可以使用具有不同代数结构的PRNG来播种不同的PRNG。例如,一些MD5散列序列。

然而,我会选择#5。如果它有效,那就没问题。如果没有,你仍然可以优化它。

重点是创建一个好的PRNG比人们预期的要困难得多。对于线程应用程序来说,一个好的PRNG很可能仍有待研究。

如果CPU的数量足够少,你可以跳过跳跃。例如,如果您有4个核心,请使用相同的值初始化所有核心,但将核心1 PRNG提前1,将#2提前3。当你需要一个新号码时,总是前进4步。

我会使用一个实例为其他实例播种。我相信你可以很容易地安全地完成这项工作。

  • 即使状态空间中的微小变化也会在下游引起相当大的变化——如果你能确保它们没有完全相同的起始空间(也没有相同的状态前缀),我就不会担心产生相同的数字。例如,只使用值1、2、3来为三个线程设定种子会很好——你甚至不需要为整个空间设定种子。另一个优点是:通过使用清晰可预测的种子,你可以很容易地否定你在挑选任何跑步的想法(假设你试图展示一些东西)
  • 以一种意味着由此产生的"孩子"高度不相关的方式播种是微不足道的。只需以广度优先的方式进行迭代;也就是说,如果你想对N x 623个int值进行种子设定,不要按顺序对623个值进行种子,而是选择第一个N并进行分配,然后再分配下一个N等。即使种子机和孩子之间存在某种相关性,不同孩子之间的相关性也应该是不存在的,这就是你所关心的
  • 我更喜欢一种尽可能允许确定性执行的算法,所以依赖于urandom是没有吸引力的。这使得调试更加容易
  • 最后,显然是测试。这些PRNG相当稳健,但无论如何都要注意结果,并根据您模拟的内容进行一些相关性测试。大多数问题都应该是显而易见的——要么你播种得很差,有明显的重复子序列,要么你播种的很好,然后质量由PRNG的限制决定
  • 对于最终执行,在完成测试后,可以使用urandom为623个状态值中的第一个种子,以获得安心和/或线程ID

种子线程1带1,种子线程2带2,等等

如果你需要蒙特卡罗,这将给你可复制的结果,很容易跟踪和实现。

看看下面的论文:伪随机数生成器的动态创建及其伴随的实现:动态创建者。它正好解决了这个问题。

如果您真的想在数学上正确,请使用SFMT算法作者提供的跳转函数。跳转函数保证两个不同PRNG流之间的序列数量最小。

然而,实际上,/dev/urandom初始化就足够了。

我认为#3是赢家。用processID或threadID之类的东西为每个线程种子;虽然从技术上讲,你可能会有重叠,但这是极不可能的。一旦你去掉个位数,即使是连续的数字也不应该与种子有关(我不知道Twister算法,但我见过的最糟糕的PRNG在7以上)。与大多数PRNG方程的范围相比,一百万个PRNG并不算多。

最后,您可以很容易地进行检查。将每个线程生成的最后种子与其他线程中的所有数字进行比较。如果种子出现在线程中,则检查每个线程中以前生成的数字;如果它们也匹配,则发生冲突,需要重新播种流并重试。

有一个具体的实现(和已发表的论文)是关于使用Mersenne Twister进行并行计算的。它是由MT的原始作者编写的。他们称之为"动态创建者",可以在这里找到:

http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/DC/DC.html

这将是一个很好的地方来研究你对MT19937的具体使用,特别是那里的论文。