Codementor Events

The ultimate System.Random guide

Published Apr 06, 2019Last updated Aug 02, 2019
The ultimate System.Random guide

Check out the original version of this article on my developer blog.

Generating a random number should be easy, right? Way back when I started learning C#, I came from a PHP background. PHP's rand was easy, just as you'd expect. It took me a while to understand what an instance of "Random" meant, and it still confuses juniors a lot, so here is my attempt at a no-bullsh*t guide on random number generation in C#.

Principles

I always believed in understanding the "why" instead of simply copying the "how", but feel free to skip to the next section if you just want the "code to copy".

Understanding Seed

For a moment forget what you know about System.Random and think of it as an IReadOnlyDictionary<int, IEnumerable<int>> in which for every integer key, there is an infinite sequence of non-negative random integer values. For example:

1 -> 534011718, 237820880, 1002897798, 1657007234, ...
2 -> 1655911537, 867932563, 356479430, 2115372437, ...
3 -> 630327709, 1498044246, 426253993, 1203643911, ...
4 -> 1752227528, 2128155929, 1211126341, 884619196, ...
...

These sequences are constants, every time you run the program you will get the exact same numbers. How can an infinite sequence be a constant? Well, the sequence itself isn't constant, but it is produced by a mathematical given an argument (what we called the dictionary key above). The actual function used is an implementation detail that may vary between frameworks and versions. The current docs.microsoft.com article says:

The chosen numbers are not completely random because a mathematical algorithm is used to select them, but they are sufficiently random for practical purposes. The current implementation of the Random class is based on a modified version of Donald E. Knuth's subtractive random number generator algorithm. For more information, see D. E. Knuth. The Art of Computer Programming, Volume 2: Seminumerical Algorithms. Addison-Wesley, Reading, MA, third edition, 1997.

This dictionary key / function argument is what we call a SEED.

It is referred to as the seed, because you "seed" the mathematical function with this number, to get a pretty-uniformly distributed sequence of numbers.

What if we just took one of those sequences and used it as our rand()?

That sequence is pretty random, but predictable. Each time you ran your application, you'd get the same sequence of numbers, not cool.

Ok, then what?

Each time we have to randomly chose which sequence which we'll be using. Wait, arn't we writing a random generator? Well obviously we cannot use our own, so we need to grab some variable from our system that is consistently changing so we don't accidentally end up with the same sequence multiple times.

It is surprisingly hard to find a random-enough variable in a computer system actually. There are two main solutions:

  1. Use multiple sensor readings (CPU temp, microphone input, mouse movement, cpu variables), combine them and use that number. This is really secure as it's unpredictable, but reading all the inputs are slow. This is how RNGCryptoServiceProvider works, which is outside the scope of this article.
  2. Use the system time. This is really fast, although somewhat predictable, but that is not a big problem for most usecases. This is the default operation mode for System.Random.

So what does System.Random do exactly?

There are two constructors of System.Random. There is one that takes an integer as the seed. It "selects" the sequence belonging to that seed, and every subsequent call to Next, NextDouble, etc. will return the upcoming number in the sequence.

The parameterless constructor is:

public Random()
      : this(Environment.TickCount)
    {}

The catch is that Environment.TickCount only changes every 10-16ms, so if you create a new instance in a short period, you will end up getting the same number sequence. (See the examples.)

Usage

OK, enough theory. Let's get to the "what and how" part.

Basic usage

Do:

Create a Random instance once, and use it multiple times.

Random r = new Random();
int a = r.Next();
int b = r.Next();
int c = r.Next();

Don't:

new a Random instance and use it in place.

int a = new Random().Next();
int b = new Random().Next();
int c = new Random().Next();

For reasons stated above this will 99% result in a, b and c being the same number.

Class usage (single-thread)

Follow this pattern for classes not intended to be used by multiple threads simultaneously. (Non thread-safe.) This is what the majority of your usages will look like.

Do:

class SomeClass{
  // initialize a single Random instance here
  private readonly Random _rng = new Random();
  
  // use it in your methods
  public ReturnType SomeMethod(){
    int randomNumber = _rng.Next();
  }
}

Don't

create the Random instance inside your method. If your method will be called multiple times within ~16ms, all calls will use the same "random" number.

Thread-safe usage

This complicates things a bit because System.Random is not thread safe.

Of course we can lock on the instance, but locking should be used as a last resort, as it can significantly affect performance, and in this case we can do better. There is absolutely no need to use the same Random instance on every thread, we can make one for each.

  private ThreadLocal<Random> _tlRng = new ThreadLocal<Random>(() => new Random());
  
  // in a method
  int randomNumber = _tlRng.Value.Next();

This is good enough most of the time, but you may think "wait, isn't there a chance that multiple threads initialize _tlRng at the same time, resulting in matching values?". There is. If you really want to go the extra mile, you can incorporate an extra value into the seed that is incremented each time an instance is created.

  private int _seedCount = 0;
  private ThreadLocal<Random> _tlRng = new ThreadLocal<Random>(() => new Random(GenerateSeed()));
  
  private static int GenerateSeed(){
    // note the usage of Interlocked, remember that in a shared context we can't just "_seedCount++"
    return (int) ((DateTime.Now.Ticks << 4) + (Interlocked.Increment(ref _seedCount)));
  }

Conclusion

This wraps our System.Random guide. Hopefully I covered all use-cases, if I didn't, or you have any questions, feel free to reach out to me. And as always, understand the internals of what you are writing, don't just copy-paste code from here, from StackOverflow, or anywhere.

Also check out why System.Random shouldn't be used for security-critical code.

Discover and read more posts from Marcell Toth
get started
post commentsBe the first to share your opinion
Bjørn Furuknap
5 years ago

Nice overview and intro.

I’d like to point out that when you want predictable randomness, for example if you are writing a game and want the same results across multiple save games to prevent reload ‘hacks’ then the built-in Random isn’t sufficiently predictable.

In this case, I would recommend looking at something like the Mersenne Twister which can be implemented across platforms and is platform and language independent. In other words, with the same seed, you would get the same results across PC, Android, Mac, Linux, in JavaScript, C#, C++, or whatever.

I wrote a game a few years ago called Twisto (now defunct) that allowed people to add their own seeds to get randomized games and share those games with the seeds across platforms, and in cases like this, the predictable randomness across platforms and languages is very important.

Marcell Toth
5 years ago

Thank you for the feedback, you bring up some really good points.

The exact algorithm System.Random uses is an implementation detail and may change between implementations, platform or even framework versions. So as you said, if you need predictability simply storing the seed isn’t enough, you have to force a fixed algorithm, too.

A nice little “feature” for this is the fact that System.Random is not sealed, and its methods are virtual. You can actually override them and use the derived class as a drop-in replacement for code that is depending on Random. For example if you need predictability you can enforce a fixed randomness-algorithm by implementing the algorithm of your choice using this method.

Of course you can just create your own separate random generator service and use that, it’s really a personal choice.

Show more replies