1

I use TBB to multithread a part of my application.
The following code was logically responsible for a non-deterministic behavior:

    std::mt19937 engine;
    std::uniform_real_distribution<double> distribution(-1., 1.);

    double x[N];

    tbb::parallel_for(0, N, [&](int i)
    {
        // ... complicated stuff done in this loop
        x[i] = distribution(engine);
    });

I changed the code to use one PRNG per thread, using TBB TLS, and seed the PRNG using the loop index.
It seems to work but looks weird to me. Is it a common practice?

    tbb::enumerable_thread_specific<std::mt19937> engine;
    std::uniform_real_distribution<double> distribution(-1., 1.);

    double x[N];

    tbb::parallel_for(0, N, [&](int i)
    {
        // ... complicated stuff done in this loop
        engine.local().seed(i);
        x[i] = distribution(engine.local());
    });
Michael M.
  • 2,556
  • 1
  • 16
  • 26
  • `parallel_for` doesn't guarantee to run each iteration on a different thread right? You're currently seeding the same engine multiple times if `local` gives you a thread local object. It will be deterministic but I'm not sure about the quality of the random numbers. – Timo Sep 07 '21 at 07:12
  • Yes, you get the point, I'm not sure about the quality of the random numbers. I know I'm seeding the same engine multiple times, but I cannot come up with a better solution. – Michael M. Sep 07 '21 at 07:23
  • I think you need to roll your own threading here with one seeded engine per-thread. – Richard Critten Sep 07 '21 at 07:26
  • How is your number actually used? Can't you put the PRNG queries in a synchronous loop before the parallel for and cache the numbers in a vector? – Timo Sep 07 '21 at 07:27
  • The numbers are used to add a perturbation to 3D point positions, so the quality of the numbers is not very important, but I do not want to write weird code. – Michael M. Sep 07 '21 at 07:30
  • @Timo thanks, I'll probably use your idea and generate a vector of random value sequentially first, and use it in the TBB loop. – Michael M. Sep 07 '21 at 07:40
  • This doesn't answer the question but what you'd need to use here is a PRNG algorithm with "jump" ability or parallel streams. See for example PCG64 or xhosiro256++. Basically what they do is directly jump to the state that they'd have after creating N random numbers, where N is a very large number - that way, you are guaranteed to get independent random numbers in each thread without strange correlations due to the similar seeding. – anymous.asker Sep 08 '21 at 03:41

1 Answers1

-1

I've not used this specific parallelization library. However with multithreading you

  • Have to read documentation on functions you use, they are usually not threadsafe unless it is explicitly stated that it is threadsafe.
  • Protect data from concurrent access. e.g by mutex or a thread yield construct.

Here is an example with mutex/lock

static std::mt19937 engine(std::random_device{}());
std::uniform_real_distribution<double> distribution(-1., 1.);
std::mutex mtx;

double x[N];

tbb::parallel_for(0, N, [&](int i)
{
    // ... complicated stuff done in this loop

    // extra scope to manage lifetime of lock
    {
        std::unique_lock<std::mutex> lock(mtx);
        x[i] = distribution(engine);
    }
});

And maybe you have to do something similar for engine seed, this may also not be threadsafe.

Pepijn Kramer
  • 9,356
  • 2
  • 8
  • 19
  • Unfortunately, I do not think it will be deterministic because the loop can be called in any order, resulting in the values of the `x` array in a different order. – Michael M. Sep 07 '21 at 07:18
  • If the innerloop is the only one accessing x[i] then that assignment is probably safe. It is the mt19937 engine that I'm trying to protect with the mutex (https://stackoverflow.com/questions/40655814/is-mersenne-twister-thread-safe-for-cpp). I also updated the answer, and now seed the generator from random_device only once that should be enough. – Pepijn Kramer Sep 07 '21 at 07:42