随机数生成器的 C++11 线程安全

Posted

技术标签:

【中文标题】随机数生成器的 C++11 线程安全【英文标题】:C++11 Thread safety of Random number generators 【发布时间】:2012-02-07 11:22:07 【问题描述】:

在 C++11 中有一堆新的随机数生成器引擎和分布函数。它们是线程安全的吗?如果您在多个线程之间共享单个随机分布和引擎,它是否安全并且您仍然会收到随机数?我正在寻找的场景类似于,

void foo() 
    std::mt19937_64 engine(static_cast<uint64_t> (system_clock::to_time_t(system_clock::now())));
    std::uniform_real_distribution<double> zeroToOne(0.0, 1.0);
#pragma omp parallel for
    for (int i = 0; i < 1000; i++) 
        double a = zeroToOne(engine);
    

使用 OpenMP 或

void foo() 
    std::mt19937_64 engine(static_cast<uint64_t> (system_clock::to_time_t(system_clock::now())));
    std::uniform_real_distribution<double> zeroToOne(0.0, 1.0);
    dispatch_apply(1000, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(size_t i) 
        double a = zeroToOne(engine);
    );

使用 libdispatch。

【问题讨论】:

【参考方案1】:

C++11 标准库是广泛的线程安全的。 PRNG 对象的线程安全保证与容器相同。更具体地说,由于 PRNG 类都是-随机的,即它们基于确定的当前状态生成确定性序列,因此实际上没有空间可以窥视或戳穿包含状态之外的任何内容(这对用户也是可见的)。

就像容器需要锁才能安全共享一样,您也必须锁定 PRNG 对象。这将使它变得缓慢且不确定。每个线程一个对象会更好。

§17.6.5.9 [res.on.data.races]:

1 本节规定了实现应满足的要求 防止数据竞争(1.10)。每个标准库函数都应 除非另有说明,否则满足每项要求。实现可能 防止在以下指定之外的情况下出现数据竞争。

2 C++ 标准库函数不得直接或间接 由当前线程以外的线程访问的访问对象(1.10) 线程,除非对象直接或间接通过 函数的参数,包括 this。

3 C++ 标准库函数不得直接或间接 修改当前线程以外的线程可访问的对象(1.10) 线程,除非对象直接或间接通过 函数的非 const 参数,包括 this。

4 [注意:例如,这意味着实现不能使用 用于内部目的的静态对象,无需同步,因为它 即使在未明确共享的程序中也可能导致数据竞争 线程之间的对象。 ——尾注]

5 C++ 标准库函数不得间接访问对象 可通过其参数或通过其容器的元素访问 参数,除非通过调用其规范所需的函数 在那些容器元素上。

6 调用标准库获得的迭代器上的操作 容器或字符串成员函数可以访问底层 容器,但不得对其进行修改。 [注:特别是容器 使迭代器无效的操作与 与该容器关联的迭代器。 ——尾注]

7 实现可以在线程之间共享它们自己的内部对象 如果对象对用户不可见并且受到数据保护 比赛。

8 除非另有规定,C++ 标准库函数应 仅在当前线程内执行所有操作,如果那些 操作具有对用户可见的效果 (1.10)。

9 [注意:这允许实现并行化操作,如果 没有明显的副作用。 ——尾注]

【讨论】:

这基本上就是我认为它不是线程安全的。是否可以共享分发对象std::uniform_real_distribution&lt;double&gt; zeroToOne(0.0, 1.0)数量线程并每个线程使用一个引擎? @user1139069:不,不安全。尽管乍一看分发对象可以通过简单地将每个调用委托给引擎对象来完成其工作,而不需要维护内部状态,但如果您考虑一下,可能需要一个不产生足够随机位的引擎被调用两次。但是两次(或一次)可能会过大,因此允许缓存多余的随机位可能会更好。 §26.5.1.6“随机数分布要求”允许这样做;分发对象特别具有随着每次调用而改变的状态。因此,出于锁定目的,它们应被视为引擎的一部分。【参考方案2】:

标准(好吧N3242)似乎没有提到随机数生成是无种族的(除了rand 不是),所以它不是(除非我错过了什么)。此外,让它们线程保存确实没有意义,因为它会产生相对较大的开销(至少与数字本身的生成相比),而不会真正赢得任何东西。

此外,我并没有真正看到拥有一个共享随机数生成器的好处,而不是每个线程都有一个,每个线程的初始化略有不同(例如,来自另一个生成器的结果或当前线程 ID)。毕竟你可能不依赖生成器在每次运行时生成特定的序列。所以我会把你的代码改写成这样(对于openmp,对libdispatch一无所知):

void foo() 
    #pragma omp parallel
    
    //just an example, not sure if that is a good way too seed the generation
    //but the principle should be clear
    std::mt19937_64 engine((omp_get_thread_num() + 1) * static_cast<uint64_t>(system_clock::to_time_t(system_clock::now())));
    std::uniform_real_distribution<double> zeroToOne(0.0, 1.0);
    #pragma omp for
        for (int i = 0; i < 1000; i++) 
            double a = zeroToOne(engine);
        
    

【讨论】:

实际上,如果从不同的线程读取相同的RNG,即使是固定种子,你也不能依赖获得相同序列的随机数,因为调度可能会导致不同的顺序在不同的运行中从不同的线程访问 RNG。所以尤其是如果你需要可重现的随机数序列,你不应该在线程之间共享RNG。 @celtschk:这取决于如何定义获得相同的序列。我会说一个会得到相同的序列(全局),只是线程会在每次运行时看到它的不同部分。 这给了我一个很好的起点!一个 obs,指定种子而不是使用系统时间+日期可能是个好主意(如果您关心可重复性)。【参考方案3】:

documentation 没有提到线程安全,所以我认为它们不是线程安全的。

【讨论】:

在 cppreference.com 上没有被提及并不意味着它不是这样。

以上是关于随机数生成器的 C++11 线程安全的主要内容,如果未能解决你的问题,请参考以下文章

使用非线程安全随机数生成器在 C 中为 pi monte carlo 更正 OpenMP 编译指示

多线程环境下生成随机数

在 C# 中创建加密随机数的最快、线程安全的方法?

初学go语言,请教生成随机数为何线程越多速度越慢?

为什么 Random.Shared 是线程安全的

C++11中的随机数生成:如何生成,它是如何工作的? [关闭]