37

In C++11 there are a bunch of new Random number generator engines and distribution functions. Are they thread safe? If you share a single random distribution and engine among multiple threads, is it safe and will you still receive random numbers? The scenario I am looking is something like,

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);
    }
}

using OpenMP or

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);
    });
}

using libdispatch.

ildjarn
  • 62,044
  • 9
  • 127
  • 211
user1139069
  • 1,505
  • 2
  • 15
  • 27

3 Answers3

28

The C++11 standard library is broadly thread safe. The thread safety guarantees on PRNG objects are the same as on containers. More specifically, since the PRNG classes are all pseudo-random, i.e. they generate a deterministic sequence based on a definite current state, there is really no room to be peeking or poking at anything outside the contained state (which is also visible to the user).

Just as containers need locks to make them safe to share, you would have to lock the PRNG object. This would make it slow and nondeterministic. One object per thread would be better.

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

1 This section specifies requirements that implementations shall meet to prevent data races (1.10). Every standard library function shall meet each requirement unless otherwise specified. Implementations may prevent data races in cases other than those specified below.

2 A C++ standard library function shall not directly or indirectly access objects (1.10) accessible by threads other than the current thread unless the objects are accessed directly or indirectly via the function’s argu- ments, including this.

3 A C++ standard library function shall not directly or indirectly modify objects (1.10) accessible by threads other than the current thread unless the objects are accessed directly or indirectly via the function’s non- const arguments, including this.

4 [ Note: This means, for example, that implementations can’t use a static object for internal purposes without synchronization because it could cause a data race even in programs that do not explicitly share objects betweenthreads. —endnote]

5 A C++ standard library function shall not access objects indirectly accessible via its arguments or via elements of its container arguments except by invoking functions required by its specification on those container elements.

6 Operations on iterators obtained by calling a standard library container or string member function may access the underlying container, but shall not modify it. [Note: In particular, container operations that invalidate iterators conflict with operations on iterators associated with that container. — end note ]

7 Implementations may share their own internal objects between threads if the objects are not visible to users and are protected against data races.

8 Unless otherwise specified, C++ standard library functions shall perform all operations solely within the current thread if those operations have effects that are visible (1.10) to users.

9 [ Note: This allows implementations to parallelize operations if there are no visible side effects. — end note ]

Potatoswatter
  • 134,909
  • 25
  • 265
  • 421
  • That's basically what I figured it wasn't thread-safe. Is it ok to share the distribution object `std::uniform_real_distribution zeroToOne(0.0, 1.0)` amount threads and use one engine per thread? – user1139069 Jan 13 '12 at 17:24
  • 1
    @user1139069: No, not safe. Although at first glance a distribution object *can* do its job by simply delegating each call to the engine object, without maintaining internal state, if you think about it an engine which doesn't produce enough random bits might need to be called twice. But twice (or once) may be overkill, so it might be better to allow caching of excess random bits. §26.5.1.6 "Random number distribution requirements" allows this; distribution objects specifically have state that changes with each call. Therefore they should be treated as part of the engine for locking purposes. – Potatoswatter Jan 14 '12 at 02:44
5

The standard (well N3242) seems to make no mention of random number generation being race free (except that rand isn't), so it isn't(unless I missed something). Besides there is really no point in having them threadsave, since it would incur a relatively hefty overhead (compared to the generation of the numbers itself at least), without really winning anything.

Furthermore I don't really see a benefit og having one shared random number generator, instead of having one per thread, each being slighly differently initialized (e.g. from the results of another generator, or the current thread id). Afterall you probably don't rely on the generator generating a certain sequence each run anyways. So I would rewrite your code as something like this (for openmp, no clue about 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);
        }
    }
}
Grizzly
  • 19,595
  • 4
  • 60
  • 78
  • 1
    Actually, if the same RNG is read from different threads, you *cannot* rely on getting the same series of random numbers even for a fixed seed because scheduling may cause a different order of access to the RNG from the different threads on separate runs. So *especially* if you need reproducible random number sequences, you should not share RNGs between threads. – celtschk Jan 11 '12 at 08:19
  • @celtschk: That depends on how one defines getting the same sequence. I would say one will get the same sequence (globaly), its just that the threads will see different parts of it with each run. – Grizzly Jan 11 '12 at 13:37
  • This gave me a nice starting point! One obs, it may be a good idea to specify a seed instead of using the system time+date (if you care about reproducibility). – gvegayon Mar 28 '18 at 21:48
1

The documentation makes no mention of thread safety, so I would assume they are not thread safe.

Mitch Wheat
  • 295,962
  • 43
  • 465
  • 541