Last Updated on September 12, 2022
You can generate (mostly) thread-safe random numbers via the random module.
In this tutorial you will discover how to use thread-safe random numbers in Python.
Let’s get started.
Need Thread-Safe Random Numbers
A thread is a thread of execution in a computer program.
Every Python program has at least one thread of execution called the main thread. Both processes and threads are created and managed by the underlying operating system.
Sometimes we may need to create additional threads in our program in order to execute code concurrently.
Python provides the ability to create and manage new threads via the threading module and the threading.Thread class.
You can learn more about Python threads in the guide:
In concurrent programming, we often need to make use of random numbers.
This may be for many reasons, such as:
- Performing a multithreaded simulation that involves random behavior.
- Performing a multithreaded experiment that involves statistical randomness.
- Performing a multithreaded statistical analysis that involves random sampling.
Therefore, we need to use random numbers, such as from the random module, in a multithreaded environment.
Are random numbers from the random module and the random.Random class thread-safe and if not, how can they be made thread-safe?
Run loops using all CPUs, download your FREE book to learn how.
Are Random Numbers Thread-Safe
At the time of writing (Python 3.10), the random module in Python is thread-safe, mostly.
Specifically, the generation of all random numbers relies on the random.random() function, which calls down to C-code and is thread-safe. You may recall that the random.random() function returns a random floating point value between 0 and 1.
Almost all module functions depend on the basic function random(), which generates a random float uniformly in the semi-open range [0.0, 1.0). Python uses the Mersenne Twister as the core generator. It produces 53-bit precision floats and has a period of 2**19937-1. The underlying implementation in C is both fast and threadsafe.
— random — Generate pseudo-random numbers
Both the functions on the “random” module are thread safe as are the methods on an instance of the random.Random class.
For example:
1 2 3 |
... # generate a thread-safe random number value = random.random() |
This means that a multithreaded program may call module functions in order to generate random numbers with a seed and sequence of random numbers shared between threads, or have a single new random.Random instance shared between threads.
The single function on the random module and the random.Random class that is not thread-safe is the gauss() function, specifically random.gauss() and random.Random.gauss().
This function will generate a random number drawn from a Gaussian distribution (e.g. normal distribution) with a given mean and standard deviation.
For example:
1 2 3 |
... # generate a thread-unsafe gaussian random number value = random.gauss(0.0, 1.0) |
This function is not thread-safe.
Multithreading note: When two threads call this function simultaneously, it is possible that they will receive the same return value. This can be avoided in three ways. 1) Have each thread use a different instance of the random number generator. 2) Put locks around all calls. 3) Use the slower, but thread-safe normalvariate() function instead.
— random — Generate pseudo-random numbers
The algorithm used for generating Gaussian random numbers will generate two numbers each time the function is called. As such, it is possible for two threads to call this function at the same time and suffer a race condition, receiving the same random Gaussian values in return.
As such the random.normalvariate() function should be used instead in a multithreaded application.
For example:
1 2 3 |
... # generate a random gaussian in a thread-safe manner value = random.normalvariate(0.0, 1.0) |
Alternatively, a mutual exclusion (mutex) lock may be used to protect the random.gauss() function via the threading.Lock class.
For example, a lock may be created once:
1 2 |
... lock = threading.Lock() |
Then used to protect each call to the random.gauss() function:
1 2 3 4 5 |
... # acquire the mutex with lock: # generate a random gaussian value = random.gauss(0.0, 1.0) |
This will ensure that only one thread can execute the random.gauss() function at a time.
You can learn more about the mutex lock in this tutorial:
Next, let’s consider best practices when using random numbers in a multithreaded Python application.
Best Practice For Threads and Random Numbers
This section considers some best practice tips when using random numbers in a multithreaded application.
They are:
- Seed the random number generator prior to generating random numbers.
- Provide each thread its own random number generator.
Let’s take a closer look at each in turn.
Seed the Random Number Generator
When using threads, it is a common requirement to need to control the sequence of random numbers generated each time the application is run to ensure reproducibility.
This is especially the case if random numbers are used in a simulation or experiment.
Reproducibility means that each time the program is run, the random number generator provides the same sequence of random numbers.
Sometimes it is useful to be able to reproduce the sequences given by a pseudo-random number generator. By re-using a seed value, the same sequence should be reproducible from run to run as long as multiple threads are not running.
— random — Generate pseudo-random numbers
This is possible because the random number generator is a deterministic equation that will generate the same sequence of pseudo or nearly-random numbers as long as it is seeded with the same value.
The seed for the pseudo random number generator can be set via the random.seed() function.
For example:
1 2 3 |
... # seed the random number generator random.seed(1) |
It is a best practice to always seed the random number generator prior to generating random numbers. The specific value of the seed does not matter, as long as it is set prior to generating the first random number in the application.
The exceptions are those applications where reproducibility is not a requirement.
Separate Random Instance Per Thread
If multiple threads access the same random number generator, it is likely that each thread will get a different subset of the random numbers from the same sequence.
As such, sharing a random number generator between threads is likely not reproducible given the non-deterministic scheduling of threads by the operating system.
This means that even though the functions on the random module and the random.Random class are thread-safe (mostly), and you seed the single random number generator, that you may still get unreproducible behavior among threads that use random numbers.
Therefore, it is a best practice to provide each thread its own instance of a random.Random class.
The random.Random assigned to each thread may then be seeded to ensure a reproducible sequence of random numbers. Care must be taken to give each thread a different seed, to ensure it receives a different sequence of random values.
This could be achieved by assigning each thread a sequence integer identifier to be used as the seed for the random number generator, e.g. 0 to n where you have n threads.
For example:
1 2 3 4 5 6 7 8 |
... # create threads for i in range(10): # create the random number generator to be used by the thread rand = random.Random(seed=i) # create the thread thread = threading.Thread(..., args=(rand,)) # ... |
Next, let’s look at some examples of using random numbers in a multi-threaded program.
Free Python Threading Course
Download your FREE threading PDF cheat sheet and get BONUS access to my free 7-day crash course on the threading API.
Discover how to use the Python threading module including how to create and start new threads and how to use a mutex locks and semaphores
Example Of Thread-Safe Random Numbers
We can explore how to generate random numbers in a multithreaded environment.
In this example, we are not interested in reproducibility, instead just demonstrating that we can use a shared pseudorandom number generator safely from many threads. We will create one thousand threads and have them all use the single built-in random number generator on the random module to generate a random number between 0 and 1, use the number to sleep for a fraction of a second, then report the value.
First, we can define a function to be executed in new threads.
The function will first generate a new random number between 0 and 1.
1 2 3 |
... # generate a thread-safe random number value = random() |
Next, it will sleep for the generated number as a fraction of a second.
1 2 3 |
... # block for a fraction of a second sleep(value) |
Then report the value generated.
1 2 3 |
... # report the value print(f'.got {value}') |
The task() function below implements this.
1 2 3 4 5 6 7 8 |
# task function that sleeps for a random fraction of a second def task(): # generate a thread-safe random number value = random() # block for a fraction of a second sleep(value) # report the value print(f'.got {value}') |
Next, in the main thread, we can generate 1,000 threads, each configured to execute the task() function, then start each thread.
1 2 3 4 5 6 7 |
... # generate a bunch of threads for i in range(1000): # create and configure the thread thread = Thread(target=task) # start the thread thread.start() |
The main thread will then wait for all threads to finish before terminating the program.
Tying this together, the complete example of many threads generating thread-safe random numbers.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
# SuperFastPython.com # example of generating thread-safe random numbers among threads from time import sleep from random import random from threading import Thread # task function that sleeps for a random fraction of a second def task(): # generate a thread-safe random number value = random() # block for a fraction of a second sleep(value) # report the value print(f'.got {value}') # generate a bunch of threads for i in range(1000): # create and configure the thread thread = Thread(target=task) # start the thread thread.start() # wait for all threads to finish |
Running the example first generates 1,000 threads and then waits for all threads to finish.
Each thread uses the same default random number generator provided by the random module. The random number generator is seeded with the time when the application was started, or close enough (e.g. the default seed).
Each thread then generates a random number using the shared random number generator, blocks for a fraction of a second and reports the value.
Below is a truncated sample of the output.
Note, your specific results will differ given the use of a different random seed.
1 2 3 4 5 6 7 8 9 10 |
.got 0.03128644107538603 .got 0.016833978413035466 .got 0.024728081787179224 .got 0.004274152560607147 .got 0.025159263330416626 .got 0.03649835202157414 .got 0.008630466364065281 .got 0.022108252200611744 .got 0.019267619443028905 ... |
Next, let’s look at how we can provide each thread its own instance of the random.Random class.
Overwhelmed by the python concurrency APIs?
Find relief, download my FREE Python Concurrency Mind Maps
Example Of Thread-Safe Random Instance
It is a good practice to give each thread its own instance the random.Random class for generating random numbers.
Each thread can then seed its own random number generator in a consistent manner.
Together, these two best practices ensure that the sequences of random numbers generated by each thread, and therefore any behavior in the application dependent upon random numbers will be reproducible.
In this example, we will demonstrate how to do this. We will create and configure ten threads. Each thread will be assigned a unique identifier from 0 to 9. This will be used to seed a random.Random instance within each thread, which will then generate a random number between 0 and 10, block for that many seconds, then report its value.
First, we can define a function that takes an integer identifier and creates a new random number, blocks and reports the value.
The task() function below implements this.
1 2 3 4 5 6 7 8 9 10 |
# task function that sleeps for a random fraction of a second def task(identifier): # create the random number generator rand = Random(identifier) # generate a random number in [0, 10] value = rand.random() * 10 # block for a fraction of ten seconds sleep(value) # report the value print(f'.thread {identifier} got {value}') |
Next, in the main thread we can loop and create, configure and start ten threads. Each thread is consistently assigned a unique identifier used for generating its own unique sequence of random numbers.
1 2 3 4 5 6 7 |
... # generate a bunch of threads for i in range(10): # create and configure the thread thread = Thread(target=task, args=(i,)) # start the thread thread.start() |
The main thread will then wait for all threads to finish before terminating the program.
Tying this together, the complete example of each thread using its own random.Random instance is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
# SuperFastPython.com # example of threads using their own random number generator from time import sleep from random import Random from threading import Thread # task function that sleeps for a random fraction of a second def task(identifier): # create the random number generator rand = Random(identifier) # generate a random number in [0, 10] value = rand.random() * 10 # block for a fraction of ten seconds sleep(value) # report the value print(f'.thread {identifier} got {value}') # generate a bunch of threads for i in range(10): # create and configure the thread thread = Thread(target=task, args=(i,)) # start the thread thread.start() # wait for all threads to finish |
Running the example first creates ten threads, each configured to execute our task() function, each with a unique seed, consistently assigned.
Each thread generates a random number between 0 and 10, blocks for that many seconds and then reports the value.
1 2 3 4 5 6 7 8 9 10 |
.thread 1 got 1.3436424411240122 .thread 8 got 2.2670585938104884 .thread 4 got 2.3604808973743454 .thread 3 got 2.3796462709189137 .thread 7 got 3.238327648331624 .thread 9 got 4.630073578150214 .thread 5 got 6.229016948897019 .thread 6 got 7.93340083761663 .thread 0 got 8.444218515250482 .thread 2 got 9.560342718892494 |
Importantly, the program is reproducible.
This means that each time we run the program, each thread will generate the same unique sequence of random numbers.
In our case, it means that we will get the same output from the program every time.
1 2 3 4 5 6 7 8 9 10 |
.thread 1 got 1.3436424411240122 .thread 8 got 2.2670585938104884 .thread 4 got 2.3604808973743454 .thread 3 got 2.3796462709189137 .thread 7 got 3.238327648331624 .thread 9 got 4.630073578150214 .thread 5 got 6.229016948897019 .thread 6 got 7.93340083761663 .thread 0 got 8.444218515250482 .thread 2 got 9.560342718892494 |
Next, let’s look at how we can make the random.gauss() function thread-safe.
Example Of Thread-Safe Gaussian Random Numbers
The random.gauss() function for generating random numbers drawn from a Gaussian distribution is not thread-safe.
In this section we will first confirm that indeed the function is not threads-safe, then show how to make the function thread safe using a mutex lock, as well as an alternate function that may be used.
Single-Threaded Gaussian Random Numbers
We can demonstrate that the random.gauss() function is not thread-safe.
One sign that generating random numbers is not thread-safe is that it returns the same random numbers in subsequent calls. Eventually the sequence of random numbers will repeat, but may take billions of function calls. Signs of duplicate numbers in a small sample of random numbers may be a sign of a race condition.
First, we can generate a large number of Gaussian random numbers in a single-threaded program to confirm that no duplicates are generated. We can then repeat the identical process using many threads, and see if a race condition occurs. If so, this confirms that the function is not thread safe.
First, we can define a function that generates many Gaussian random numbers and adds them to a provided set. We use a set, because it will only store unique values.
1 2 3 4 5 6 7 8 |
# task that generates many gaussian random numbers and adds them to a set def task(values_set): # repeat many times for i in range(10000): # generate a thread-unsafe gaussian random number value = gauss(0.0, 1.0) # store the value values_set.add(value) |
We can fix the seed for the random number generator, to ensure we generate the same sequence of numbers each time the code is run.
1 2 3 |
... # seed the random number generator seed(1) |
Next, we can then call this function many times and provide it a set to populate.
1 2 3 4 5 6 |
... # maintain a set of all generated numbers values = set() # generate many gaussian random values and add them to the set for _ in range(1000): task(values) |
Finally, we can report the total number of unique random numbers generated.
1 2 3 |
... # report the number of unique gaussian random numbers print(f'Generated {len(values)} unique Gaussian random numbers') |
Given that the sample of random numbers is small, we would not expect any duplicates.
Therefore, 1000 * 10,000 would provides 10,000,000 unique values
Tying this together, the complete example of generating a modest sample of Gaussian random numbers without threads is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
# SuperFastPython.com # example of generating gaussian random numbers from random import seed from random import gauss # task that generates many gaussian random numbers and adds them to a set def task(values_set): # repeat many times for i in range(10000): # generate a thread-unsafe gaussian random number value = gauss(0.0, 1.0) # store the value values_set.add(value) # seed the random number generator seed(1) # maintain a set of all generated numbers values = set() # generate many gaussian random values and add them to the set for _ in range(1000): task(values) # report the number of unique gaussian random numbers print(f'Generated {len(values)} unique Gaussian random numbers') |
Running the example first seeds the module random number generator, then generates a many random numbers drawn from a Gaussian distribution.
In this case, we can see that indeed 10,000,000 unique values were generated.
1 |
Generated 10000000 unique Gaussian random numbers |
Next, let’s add Threads in an effort to force a race-condition.
Thread-Unsafe Gaussian Random Numbers
The example from the previous section can be updated to use threads in an effort to force a race condition.
We will know that we have a race condition if the set of generated numbers does not equal 10 million. A value less than this will indicate that some duplicate numbers were generated due to the known race condition with the random.gauss() function.
The example can be updated so that instead of calling the task() function 1,000 times in a loop, instead, we create 1,000 threads that each call the function once.
This can be achieved by creating 1,000 threading.Thread instances configured to call the task() function via the “target” argument and passing the set for collecting values via the “args” argument.
1 2 3 |
... # create and configure many threads threads = [Thread(target=task, args=(values,)) for i in range(1000)] |
Note, the task() function calls add() on the set shared among all threads. This function is atomic and thread safe.
For more on atomic options, see the tutorial:
We can then start all threads and then wait for them to complete in the main thread.
1 2 3 4 5 6 7 |
... # start all threads for thread in threads: thread.start() # wait for all threads to finish for thread in threads: thread.join() |
Tying this together, the updated but thread-unsafe version of generating Gaussian random numbers is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
# SuperFastPython.com # example of thread-unsafe gaussian random numbers from random import seed from random import gauss from threading import Thread # task that generates many gaussian random numbers and adds them to a set def task(values_set): # repeat many times for i in range(10000): # generate a thread-unsafe gaussian random number value = gauss(0.0, 1.0) # store the value values_set.add(value) # seed the random number generator seed(1) # maintain a set of all generated numbers values = set() # create and configure many threads threads = [Thread(target=task, args=(values,)) for i in range(1000)] # start all threads for thread in threads: thread.start() # wait for all threads to finish for thread in threads: thread.join() # report the number of unique gaussian random numbers print(f'Generated {len(values)} unique Gaussian random numbers') |
Running the example first creates 1,000 threads all configured to execute the task() function.
The threads are then started and the main thread waits for them to complete.
Each thread generates 10,000 Gaussian random numbers and adds them to the shared set.
In this case, we can see that less than 10 million unique random numbers were generated, suggesting that a few race conditions occurred.
1 |
Generated 9999995 unique Gaussian random numbers |
In fact, each time the example is run, a different number of unique random numbers are generated.
1 |
Generated 9999979 unique Gaussian random numbers |
Next, let’s look at how we can update the example to make it thread-safe.
Thread-Safe Gaussian Random Numbers
The example in the previous section showed that the random.gauss() function is not thread safe.
We can update the example so that it is thread-safe.
This can be achieved by creating a mutual exclusion (mutex) lock and sharing it among all threads. The lock can then be used to protect the random.gauss() function so that only one thread can call the function at a time.
You can learn more about protecting critical sections with mutex locks in this tutorial:
First, we can update the task() function to take a shared lock as an argument.
1 2 3 4 |
... # task that generates many gaussian random numbers and adds them to a set def task(values_set, lock_gauss): # ... |
We can then acquire the lock via the context manager prior to generating a random number.
1 2 3 4 |
... # generate a thread-unsafe gaussian random number with lock_gauss: value = gauss(0.0, 1.0) |
The updated version of the task() function with these changes is listed below.
1 2 3 4 5 6 7 8 9 |
# task that generates many gaussian random numbers and adds them to a set def task(values_set, lock_gauss): # repeat many times for i in range(10000): # generate a thread-unsafe gaussian random number with lock_gauss: value = gauss(0.0, 1.0) # store the value values_set.add(value) |
In the main thread we can create a threading.Lock instance and pass it to each thread as an argument.
1 2 3 4 5 |
... # create a shared lock lock_gauss = Lock() # create and configure many threads threads = [Thread(target=task, args=(values, lock_gauss)) for i in range(1000)] |
Tying this together, the complete example of generating Gaussian random numbers in a thread-safe manner is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
# SuperFastPython.com # example of thread-safe gaussian random numbers from random import seed from random import gauss from threading import Thread from threading import Lock # task that generates many gaussian random numbers and adds them to a set def task(values_set, lock_gauss): # repeat many times for i in range(10000): # generate a thread-unsafe gaussian random number with lock_gauss: value = gauss(0.0, 1.0) # store the value values_set.add(value) # seed the random number generator seed(1) # maintain a set of all generated numbers values = set() # create a shared lock lock_gauss = Lock() # create and configure many threads threads = [Thread(target=task, args=(values, lock_gauss)) for i in range(1000)] # start all threads for thread in threads: thread.start() # wait for all threads to finish for thread in threads: thread.join() # report the number of unique gaussian random numbers print(f'Generated {len(values)} unique Gaussian random numbers') |
Running the example first initializes the shared random number generator as before, then creates the shared set and lock.
A total of 1,000 new threads are created and configured to run the task() function.
The main thread blocks until all new threads finish, then reports the total number of unique values generated.
In this case, we can see that the race condition was fixed and that the expected 10 million unique values were generated. Importantly, the same result is achieved each time the program is run.
1 |
Generated 10000000 unique Gaussian random numbers |
Note, this version is significantly slower than the thread-unsafe version. This version took about 3 minutes on my system compared to about 7 seconds for the thread-unsafe version.
Alternate Thread-Safe Gaussian Random Numbers
An alternate approach to generating thread-safe Gaussian random numbers is to use the random.normalvariate() function.
This function performs the same task as random.gauss() except that it is thread-safe.
The thread-safety is achieved by not using any internal state for efficiency as is done within the random.gauss() function.
We can update the thread-unsafe version above to use this function instead, and confirm that it is indeed thread-safe.
For example, we can change the random.gauss() function in our task() function to call random.normalvariate() directly:
1 2 3 4 5 6 7 8 |
# task that generates many gaussian random numbers and adds them to a set def task(values_set, lock_gauss): # repeat many times for i in range(10000): # generate a thread-unsafe gaussian random number value = normalvariate(0.0, 1.0) # store the value values_set.add(value) |
Tying this together, the complete example of using an alternate function for generating Gaussian random numbers is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
# SuperFastPython.com # example of thread-safe alternative to gaussian random numbers from random import seed from random import normalvariate from threading import Thread # task that generates many gaussian random numbers and adds them to a set def task(values_set): # repeat many times for i in range(10000): # generate a thread-unsafe gaussian random number value = normalvariate(0.0, 1.0) # store the value values_set.add(value) # seed the random number generator seed(1) # maintain a set of all generated numbers values = set() # create and configure many threads threads = [Thread(target=task, args=(values,)) for i in range(1000)] # start all threads for thread in threads: thread.start() # wait for all threads to finish for thread in threads: thread.join() # report the number of unique gaussian random numbers print(f'Generated {len(values)} unique Gaussian random numbers') |
Running the example creates and starts 1,000 new threads, each executing our task() function.
The main thread blocks until all threads finish, then report the total number of unique random numbers that were generated.
We can see that the function does appear thread-safe, as it generated the 10 million unique values as expected. Importantly, it achieves the same result each time the code is run.
1 |
Generated 10000000 unique Gaussian random numbers |
Note, using the random.normalvariate() function is dramatically faster than using a mutex lock as in the previous section. On my system it took about 8 seconds compared to 3 minutes with the lock.
Further Reading
This section provides additional resources that you may find helpful.
Python Threading Books
- Python Threading Jump-Start, Jason Brownlee (my book!)
- Threading API Interview Questions
- Threading Module API Cheat Sheet
I also recommend specific chapters in the following books:
- Python Cookbook, David Beazley and Brian Jones, 2013.
- See: Chapter 12: Concurrency
- Effective Python, Brett Slatkin, 2019.
- See: Chapter 7: Concurrency and Parallelism
- Python in a Nutshell, Alex Martelli, et al., 2017.
- See: Chapter: 14: Threads and Processes
Guides
- Python Threading: The Complete Guide
- Python ThreadPoolExecutor: The Complete Guide
- Python ThreadPool: The Complete Guide
APIs
References
Takeaways
You now know how to use random numbers in a multithreaded application in Python.
Do you have any questions?
Ask your questions in the comments below and I will do my best to answer.
Photo by Atom Riders on Unsplash
Do you have any questions?