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

37

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。

3个回答

28
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.通过调用标准库容器或字符串成员函数获取的迭代器上的操作可以访问底层容器,但不得修改它。[注:特别地,使迭代器无效的容器操作与对该容器元素的操作冲突。

1 迭代器是一种访问容器元素的方式,每个迭代器关联着容器中的一个元素。[与该容器相关的迭代器见尾注]

2 容器的迭代器遍历所有元素的顺序是固定的,且不会因为插入或删除元素而改变。

3 每种容器都有自己特定的迭代器类型,但它们都遵循相同的接口规范。

4 开发者可以使用迭代器进行读写操作,从而实现对容器内元素的操作。

5 在多线程环境下,为了保证数据的一致性,开发者需要采取措施来避免迭代器的数据竞争。

6 [尾注:每个容器关联的迭代器有其特定要求,请查看相关文档。]

7 如果某些内部对象对用户不可见且受到数据竞争保护,实现可以在线程之间共享这些对象。

8 除非另有说明,C++标准库函数在执行影响用户可见的操作时,应仅在当前线程中执行。

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


这基本上就是我想到的它不是线程安全的。在多个线程中共享分布对象 std::uniform_real_distribution<double> zeroToOne(0.0, 1.0) 并为每个线程使用一个引擎,这样做可以吗? - user1139069
1
@user1139069:不,不安全。虽然乍一看,分布对象可以通过简单地将每个调用委托给引擎对象来完成其工作,而无需维护内部状态,但如果您考虑一下,一个不产生足够随机位的引擎可能需要被调用两次。但是两次(或一次)可能过度,因此最好允许缓存多余的随机位。§26.5.1.6“随机数分布要求”允许这样做;分布对象具有随着每次调用而更改的状态。因此,它们应视为锁定目的引擎的一部分。 - Potatoswatter

5

标准文档(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);
        }
    }
}

1
实际上,如果从不同的线程读取相同的 RNG,则即使对于固定种子,也不能依赖于获得相同的随机数序列,因为调度可能导致不同的线程在单独运行时以不同的访问顺序访问 RNG。因此,特别是如果您需要可重复的随机数序列,则不应在线程之间共享 RNG。 - celtschk
@celtschk:这取决于如何定义获得相同的序列。我认为一个人会获得相同的序列(全局),只是线程在每次运行时看到的不同部分不同。 - Grizzly
这给了我一个不错的起点!有一个小建议,如果你关心可重复性,最好指定一个种子而不是使用系统时间+日期。 - gvegayon

1

文档中没有提到线程安全性,因此我会认为它们不是线程安全的。


14
未被cppreference.com提及并不代表它不存在。 - Potatoswatter

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