Saturday, 18 May 2019

A thread-safe global random number generator using C++ <random> and <mutex>

In a previous post, Sandro showed how to use the C++ <random> header to define a global random number generator (RNG). If at some point the user decides to parallelize their program, they would have to make the global RNG thread-safe. This requires only a small modification.

I have copied Sandro's code and added thread-safety using the C++ (with standard at least c++11) headers <thread> and <mutex>. In the header file thread_safe_random.hpp, the functions RANDOM and Seed are declared.

#ifndef _RND_HH_
#define _RND_HH_
void Seed(int seed);
double RANDOM();
#endif
The C++ file thread_safe_random.cpp defines the functions RANDOM and Seed, and the RNG and uniform distribution as before. Additionally, a mutex my_rng_mutex is defined to guard my_rng. I am using a lock_guard to lock and release the mutex. When one thread of execution calls the function RANDOM, it acquires the mutex. Any other thread that calls RANDOM, has to wait until the mutex is released.

#include <iostream>
#include <chrono>
#include <random> // requires flag --std=c++11 (or higher)
#include <mutex> // requires flag -pthread
#include "thread_safe_random.hpp"
std::mt19937_64 my_rng; // Defines an engine
std::uniform_real_distribution<double> my_unif_real_dist(0.0, 1.0); // Define distribution
std::mutex my_rng_mutex; // a mutex to guard my_rng
// This is the function to call if you want a random number in the interval [0,1)
double RANDOM() {
std::lock_guard<std::mutex> lock(my_rng_mutex);
return my_unif_real_dist(my_rng);
// mutex is released when lock goes out of scope
}
/* Function to seed the random number generator from main file
* This function again locks the mutex for when a thread decides
* to re-seed the RNG.
*/
void Seed(int seed) {
std::lock_guard<std::mutex> lock(my_rng_mutex);
my_rng.seed(seed);
// mutex is released when lock goes out of scope
}
In order to demonstrate thread_safe_random, I created max threads in the main function that use the auxiliary function fetch_random_number to call RANDOM.

#include <iostream>
#include <vector>
#include <thread>
#include "thread_safe_random.hpp"
// auxiliary function for the construction of std::thread
void fetch_random_number(double & x) {
x = RANDOM();
}
int main() {
int max = 10;
int my_seed = 235;
Seed(my_seed);
// a vector that will contain the threads
std::vector<std::thread> threads(max);
// a vector to store the results
std::vector<double> xs(max);
// create threads of execution that fetch a random number
for ( int i = 0; i < max; ++i ) {
// NB: xs[i] has to be wrapped in std::ref to pass a reference
threads[i] = std::thread(fetch_random_number, std::ref(xs[i]));
}
// synchonize and print the results
for ( int i = 0; i < max; ++i ) {
threads[i].join();
// when join() returns, xs[i] contains a fresh random number
std::cout << xs[i] << std::endl;
}
return 0;
}
view raw main.cpp hosted with ❤ by GitHub
The result should look like
$ g++ --std=c++11 -pthread main.cpp thread_safe_random.cpp -o test_safe_random
$ ./test_safe_random
0.284779
0.243487
0.161906
0.338338
0.235765
0.502853
0.389262
0.165401
0.244871
0.194046
However, the order of these numbers can change each time you execute the program. This means that the program is no longer deterministic (although we can argue about what it means to be deterministic or not), because the OS determines the order in which the threads call RANDOM. Another problem with this implementation is that it will be slow when each thread needs many random numbers.

No comments:

Post a Comment