5

I have a bunch of threads, each one needs a thread safe random number. Since in my real program threads are spawned and joined repeatedly, I wouldn't like to create random_device and mt19937 each time I enter a new parallel region which calls the same function, so I put them as static:

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

I cannot place them as threadprivate because of Error C3057: dynamic initialization of 'threadprivate' symbols is not currently supported. Some sources say random_device and mt19937 are thread safe, but I haven't managed to find any docs which would prove it.

  1. Is this randomization thread safe?
  2. If no, which of the static objects can be left as static to preserve thread safety?
17
  • First guess: No. Because threads can be interrupted during the non-atomic operation of uni(rng) and interfere with random number generation. rd, rng and uni are objects and their states are shared across multiple threads without synchronization. Commented May 3, 2022 at 11:20
  • You can certainly make them static thread_local if that helps, not sure about openmp compatibility. If rd is used only for initialization, consider rng(std::random_device{}())); Commented May 3, 2022 at 11:22
  • 4
    Yes, both mt19937 and uni have state inside them that they modify to create the random numbers. Because they modify themselves, they need to be protected if you are sharing them between threads. Commented May 3, 2022 at 12:10
  • 2
    Some sources say random_device and mt19937 are thread safe, but I haven't managed to find any docs which would prove it. -- Those "docs" are the C++11 standard. Local static variables are guaranteed to be accessed in a thread-safe manner, otherwise Meyers singletons would not work. It is the uni(rng) that is the issue. Commented May 3, 2022 at 12:41
  • 2
    @paleonix That's not the right language. (Even if a solution in that language would work for this language). Commented May 3, 2022 at 14:02

2 Answers 2

1

Here is a different approach. I keep a global seeding value so that the random_device is only used once. Since using it can be very slow, I think it is prudent to only use it as rarely as possible.

Instead we increment the seeding value per thread and also per use. That way we avoid the birthday paradox and we minimize the thread-local state to a single integer.

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

See also this: How to make this code thread safe with openMP? Monte Carlo two-dimensional integration

For the approach of just increment the seeding value, see this: https://www.johndcook.com/blog/2016/01/29/random-number-generator-seed-mistakes/

Sign up to request clarification or add additional context in comments.

2 Comments

If the quality of the random numbers is important I would advise reading Melissa O'Neills PCG blog.
You might usefully also consider a random number generator which is intended to be used in parallel. See, for instance, Parallel Random Numbers: As Easy as 1, 2, 3 which is now implemented in many maths libraries.
0

I think threadprivate is the right approach still, and you can obviate the initialization problem by doing a parallel assignment later.

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

Btw, note the stringstream: OpenMP has a tendency to split output lines at the << operators.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.