LinkedIn: "Are you still using new Random() everywhere?"

10/27/2025
4 minute read

You need some advice around Random? Sure there is something on LinkedIn for that. Let's have a look.

new Random is evil!!!!11111

In this LinkedIn post, the Author does claim the following (paraphrased)

  • new Random uses system clock
  • if multiple instances are created in a tight loop, they generate identical results.

The claim is then supported by the following code:

Random → Thread Unsafe:

for(var i = 0; i < 5;i++)
{
    var random = new Random();
    Console.WriteLine(random.Next(1,100));
}
// Output 42, 42, 42, 42, 42

Random.Shared → Thread Safe:

for(var i = 0; i < 5;i++)
{
    var random = new Random();
    Console.WriteLine(random.Next(1,100));
}
// Output: 17, 82, 19, 23, 5

First things first, a for loop has nothing to do with threads. But maybe that is just a small mistake. Because if you run the "Thead Unsafe" version: The output is absolutely not the same. If you want to try it out yourself: Be my guest. So where is this coming from?

.NET Framework times

In .NET Framework, new Random was relying on. DateTime.Now.Ticks which was resolved roughly every 16ms. So in a timeframe of 16 milliseconds you get the same seed, leading to the same Next(...) result everytime. But that isn't true anymore since the netcore times and especially since modern versions of the framework. Meet:

Xoshiro256**

Let's have a look into the Random class. For the new Random ctor we find:

public Random() =>
	// With no seed specified, if this is the base type, we can implement this however we like.
	// If it's a derived type, for compat we respect the previous implementation, so that overrides
	// are called as they were previously.
	_impl = GetType() == typeof(Random) ? new XoshiroImpl() : new CompatDerivedImpl(this);

What that means is that if we don't have a derived type (therefore the check GetType() == typeof(Random)) we are using XoshiroImpl that is defined here.

This bad boy is using system calls internally:

internal static partial class Interop
{
    internal static partial class Sys
    {
        [LibraryImport(Interop.Libraries.SystemNative, EntryPoint = "SystemNative_GetNonCryptographicallySecureRandomBytes")]
        internal static unsafe partial void GetNonCryptographicallySecureRandomBytes(byte* buffer, int length);
 
        // ...
 
    internal static unsafe void GetRandomBytes(byte* buffer, int length)
    {
        Sys.GetNonCryptographicallySecureRandomBytes(buffer, length);
    }
 
    // ....
}

So at best you are at the mercy of your underlying operating system. So let's continue and find out how thread safe we are here. We could easily check what Random.Shared does, because, as the author said, it is thread safe.

Provides a thread-safe Random instance that may be used concurrently from any thread.

Random.Shared is using the following implementation:

public static Random Shared { get; } = new ThreadSafeRandom();

What a surprise 😄. Okay what is ThreadSafeRandom?

Basically:

[ThreadStatic]
private static XoshiroImpl? t_random;

public ThreadSafeRandom() : base(isThreadSafeRandom: true) { }

private static XoshiroImpl LocalRandom => t_random ?? Create();

It is the same implementation, but using a ThreadStatic attribute. Keep that in mind as comes into play later.

Is new Random now thread safe?

The answer it: It depends. And it depends on how you use it. For that let's dig out a beautiful blog post from Andrew lock from 2022: "Working with System.Random and threads safely in .NET Core and .NET Framework".

He has a nice way of showing if we have threading issues (somewhat in the middle of the blog post). So let me "borrow" his code here:

using System;
using System.Linq;
using System.Threading.Tasks;

// ? This isn't safe, don't do it ?
Random rng = new(); // create a shared Random instance
Parallel.For(0, 10, x =>  // run in parallel
{
    var numbers = new int[10_000];
    for (int i = 0; i < numbers.Length; ++i)
    {
        numbers[i] = rng.Next(); // Fetch 10,000 random numbers, to trigger the thread-safety issues
    }

    var numZeros = numbers.Count(x => x == 0); // how many issues were there?
    Console.WriteLine($"Received {numZeros} zeroes");
});

If you have threading issues, you get multiple zero counts. You can also run it as such (moving the new operator inside the loop):

using System;
using System.Linq;
using System.Threading.Tasks;

// ? This isn't safe, don't do it ?
Parallel.For(0, 10, x =>  // run in parallel
{
	  Random rng = new(); // create a non-shared Random instance
    var numbers = new int[10_000];
    for (int i = 0; i < numbers.Length; ++i)
    {
        numbers[i] = rng.Next(); // Fetch 10,000 random numbers, to trigger the thread-safety issues
    }

    var numZeros = numbers.Count(x => x == 0); // how many issues were there?
    Console.WriteLine($"Received {numZeros} zeroes");
});

The second example is more what the author does. Remember the ThreadLocalAttribute. That is the difference between version 1 and 2. In version 2, we create new instances for each call. ThreadLocal has no play here. And guess what: No threading issues. In such case Random.Shared and new Random() would behave the same (except the useless allocations which you shouldn't have).

Only version 1 is different, because in version 1 the ThreadLocalAttribute would tell that for each of the threads you would do a new Random() kind of thing.

Conclusion

I guess I took a lot of your time to be just a dick and correct things, I probably shouldn't have to. But hey, hope you learned something.

An error has occurred. This application may no longer respond until reloaded. Reload x