Working with System.Random and threads safely in .NET Core and .NET Framework

In this post I look at some of the ways you can misuse System.Random, comparing .NET Framework, NET Core, and .NET 6 implementations.

In this post I look at some of the ways you can misuse System.Random for generating random numbers, specifically around thread safety. I start by showing how to use the built-in thread-safe Random generator in .NET 6. I then step back to previous .NET Core implementations, before the thread-safe Random generator was added, and show how to add your own. Finally, we take one more step back to .NET Framework, and look at he issues that arise there.

tl;dr; If you're using .NET 6, then always use the static property Random.Shared if possible. If (like me) you need to support older version of .NET Core and .NET Framework, then read on!

Generating random numbers from multiple threads in .NET 6+

It's a common requirement to be able to generate some sort of random number in .NET. If you don't need this to be a cryptographically secure random number then System.Random is the class you want. This can generate random bytes, integers, doubles, depending on what you require.

If you do need a cryptographically secure random number then look at the System.Security.Cryptography.RandomNumberGenerator class.

The humble Random class has been around since .NET Framework 1.0, but an important thing to bear in mind is that it's not thread-safe by default. We'll explore this in more detail throughout this post, and look at the various ways you can incorrectly use Random

In .NET 6, however, a new static property was added to Random, called Shared, which avoids all the pitfalls I'm going to describe. If you're generating random numbers in .NET 6+, you don't need to control the "seed" value, and you need thread-safe access, then Random.Shared is the way to go.

The following .NET 6 example is a simple demonstration of how to use Random.Shared. It doesn't rigorously put the thread-safety to the test, it just demonstrates that it's safe to use in parallel scenarios, where thread-safety is a requirement:

using    System;  using System.Threading.Tasks;    // Run the lambda in parallel  Parallel.For(0, 10, x =>  {      // use the shared instance to generate a random number      var value = Random.Shared.Next();       Console.WriteLine(value);  });  

When you run this, as expected, you get a bunch of random numbers printed to the console as you might expect.

345470185  1790891750  1620523217  786846218  2095595672  961899483  1987563145  1728945026  813751074  1542500379  

All very standard, but the important point here is that you must use the Random.Shared instance. But what if you're not using .NET 6+? Then you don't have a shared instance. And what about if you need to be able to control the seed used? Random.Shared doesn't let you do this, so you need to take a different approach.

The thread-safety problem in .NET Core / .NET 5

The naΓƒ¯ve user of Random that needs a single shared random number generator might be tempted to do something like the following:

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 value = rng.Next(); // grab the next random number      Console.WriteLine(value);  });  

If you test this, you likely won't find any obvious problems. Even though this code is not thread-safe, it likely won't show any issues. Until laterΓ’€¦

The following sample (based on this SO post) uses brute-force to trigger a thread-safety issue, where multiple threads are running rng.Next() at once. The defined behaviour of Random is to return 0 if a thread-safety issue is detected, so the following code prints out how many thread-safety issues were detected:

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 run this code with dotnet run -c Release (you're less likely to see issues in Debug mode) you'll get output that looks something like this:

Received 8383 zeroes  Received 7851 zeroes  Received 9044 zeroes  Received 8828 zeroes  Received 10000 zeroes  Received 10000 zeroes  Received 4261 zeroes  Received 10000 zeroes  Received 10000 zeroes  Received 10000 zeroes  

Yikes! This shows the scale of the problemΓ’€"in some of those loops, every call to Next() returned 0. When Random says it's not thread-safe, it's really not lying!

As an aside, if you run the above code in .NET 6_, then you will see there aren't any thread safety issues, even though you're not using Random.Shared. This PR made the default constructor case (where you don't specify a seed number for Random) thread-safe. However, if you do provide a seed, e.g. using Random rng = new(), you'll see the exact same behaviour!

Ok, so we've established you can't just use a shared Random instance, so what's the solution?

Using Random in a thread-safe manne r in .NET Core / .NET 5

One naΓƒ¯ve solution is to simply not share any Random instances. For example, you could rewrite the above example to create a new Random instance inside the loop:

using System;  using System.Linq;  using System.Threading.Tasks;    // Γ’š  This isn't safe in .NET Framework, don't do it Γ’š   Parallel.For(0, 10, x =>  // run in parallel  {      Random rng = new(); // Γ°Ÿ'ˆ create a private 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      }        var numZeros = numbers.Count(x => x == 0); // how many issues were there?      Console.WriteLine($"Received {numZeros} zeroes"); // always 0 issues  });  

As mentioned in the code sample, this isn't safe in .NET Framework, though it is in .NET Core. You'll see why it's not safe later.

The Random instance is created inside the loop, so there are no shared instances and no thread-safety issues:

Received 0 zeroes  Received 0 zeroes  Received 0 zeroes  Received 0 zeroes  Received 0 zeroes  Received 0 zeroes  Received 0 zeroes  Received 0 zeroes  Received 0 zeroes  Received 0 zeroes  

This solution is "fine", but if you're going to be calling this code a lot, you'll be creating a lot of Random instances which need to be garbage collected etc.

We can create a better solution if we're willing to wrap Random in our own type. In the following example, we use [ThreadStatic] to create one instance of Random per thread. That means we do reuse Random instances, but all access is always from a single thread, so it's guaranteed to be thread safe. This way we create at most n instances, where n is the number of threads.

using System;  internal static class ThreadLocalRandom  {      [ThreadStatic]      private static Random? _local; // only accessed         public static Random Instance      {          get          {              if (_local is null)              {                  _local = new Random();              }                return _local;          }      }  }  

You could then update your usages as follows:

using System;  using System.Linq;  using System.Threading.Tasks;    // Γ’š  This isn't safe in .NET Framework, don't do it Γ’š   Parallel.For(0, 10, x =>  // run in parallel  {      Random rng = ThreadLocalRandom.Instance; // Γ°Ÿ'ˆ Use the shared instance for the tread      var numbers = new int[10_000];      for (int i = 0; i < numbers.Length; ++i)      {          numbers[i] = rng.Next(); // Fetch 10,000 random numbers      }        var numZeros = numbers.Count(x => x == 0); // how many issues were there?      Console.WriteLine($"Received {numZeros} zeroes"); // always 0 issues  });  

Note that with this simple example it's still possible to run into thread-safety issues if you pass the ThreadLocalRandom.Instance between threads. For example, the following shows the same issue as before:

using System;  using System.Linq;  using System.Threading.Tasks;    // Γ’š  This isn't safe any time! Don't do it Γ’š   Random rng = ThreadLocalRandom.Instance; // Γ°Ÿ'ˆ Using the shared instance for ALL threads Γ’š   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(); // Γ°Ÿ'ˆ Every loop uses the same Random instance      }        var numZeros = numbers.Count(x => x == 0); // how many issues were there?      Console.WriteLine($"Received {numZeros} zeroes"); // Lots and lots of issues!  });  

The above example is why I prefer to provide an implementation that completely wraps Random. The following ThreadSafeRandomNetCore wraps the Random methods we need, never exposing the Random instance to callees, so avoids any issues like the ones above. For example, the ThreadSafeRandomNetCore below, doesn't expose Random, it wraps the call to Next() instead:

using System;  // Γ’š  This isn't safe in .NET Framework, so don't use it Γ’š   internal static class ThreadSafeRandomNetCore  {      [ThreadStatic]      private static Random? _local;        private static Random Instance      {          get          {              if (_local is null)              {                  _local = new Random();              }                return _local;          }      }        public static int Next() => Instance.Next();  }  

To use this class, simply replace the instance calls to rng.Next() with calls to the static ThreadSafeRandomNetCore.Next()

using System;  using System.Linq;  using System.Threading.Tasks;    // Γ’š  This isn't safe in .NET Framework, don't do it Γ’š   Parallel.For(0, 10, x =>  // run in parallel  {      var numbers = new int[10_000];      for (int i = 0; i < numbers.Length; ++i)      {          numbers[i] = ThreadSafeRandomNetCore.Next(); // Γ°Ÿ'ˆ Call the static helper instead      }        var numZeros = numbers.Count(x => x == 0); // how many issues were there?      Console.WriteLine($"Received {numZeros} zeroes"); // always 0 issues  });  

Note that this is pretty much exactly how Random.Shared is implemented!

We've now tackled all the thread-safety issues, but the elephant in the room that I keep having to mention is that this isn't yet safe on .NET Framework. In the next section we look at why, and how to solve the issue.

The trouble with Random on .NET Framework

The main problem with Random on .NET Framework is well documented, and comes down to two facts:

  • When you call new Random() on. NET Framework, it uses the system -clock to generate a seed value.
  • The system-clock has finite resolution.

These two facts combine to mean that if you call new Random() in quick succession on .NET Framework, you can end up with two Random instances with the same seed value, which means the Random instances will return identical sequence of numbers Γ°Ÿ˜±

You can reproduce this easily by creating a .NET Framework console app:

<Project Sdk="Microsoft.NET.Sdk">      <PropertyGroup>      <OutputType>Exe</OutputType>      <TargetFramework>net461</TargetFramework>      <LangVersion>latest</LangVersion>    </PropertyGroup>    </Project>  

And generating Random instances in quick succession:

using System;  using System.Threading.Tasks;    Parallel.For(0, 10, x =>  {      Random rnd = new(); // Creating a new instance      var value = rnd.Next();      Console.WriteLine(value);  });  

When run (dotnet run -c Release), this prints something like the following, with lots of duplicate values:

839229964   #Γ°ŸŸ©  839229964   #Γ°ŸŸ©  1258386001  #Γ°ŸŸ¥  2028913929  #Γ°ŸŸ¦  300586319   #Γ’¬œ  300586319   #Γ’¬œ  1258386001  #Γ°ŸŸ¥  839229964   #Γ°ŸŸ©  1258386001  #Γ°ŸŸ¥  2028913929  #Γ°ŸŸ¦  

Even though we've created 10 instances, there's only 4 different seed values here. That's Not GoodΓ’„¢.

This problem is not solved by the previous ThreadSafeRandomNetCore implementation, as the problem is creating multiple Random instances on different threads, which the previous ThreadSafeRandomNetCore does not guard against.

The solution to this is well known: you create a single Random instance which is solely used to provide seed values for the remaining Random instances. As the seed values are random, the Random instances are uncorrelated, and you can avoid the above issue entirely.

This is the initialization approach used by default in .NET Core, which is why you only see the issue in .NET Framework

We can implement the same approach in the ThreadSafeRandom wrapper class, shown below. This class uses the above approach to create the [ThreadStatic] instances, avoiding the initialization problem in .NET Framework.

#nullable enable  using System;  using System.Threading.Tasks;    internal static class ThreadSafeRandom  {      [ThreadStatic]      private static Random? _local;      private static readonly Random Global = new(); // Γ°Ÿ'ˆ Global instance used to generate seeds        private static Random Instance      {          get          {              if (_local is null)              {                  int seed;                  lock (Global) // Γ°Ÿ'ˆ Ensure no concurrent access to Global                  {                      seed = Global.Next();                  }                    _local = new Random(seed); // Γ°Ÿ'ˆ Create [ThreadStatic] instance with specific seed              }                return _local;          }      }        public static int Next() => Instance.Next();  }  

With the above approach, you now have a thread-safe implementation that can be used in all versions of the framework. You can update your usages to use ThreadSafeRandom:

using System;  using System.Threading.Tasks;    Parallel.For(0, 10, x =>  {      var value = ThreadSafeRandom.Next(); // Γ°Ÿ'ˆ Use ThreadSafeRandom directly      Console.WriteLine(value);  });  

and all your thread-safety Random issues will melt away, even on .NET Framework!

If you're using .NET 6+, I still suggest you use the built-in Random.Shared, but if you're not so lucky, you can use ThreadSafeRandom to solve your issues. If you're targeting both .NET 6 and other frameworks you could always use #if directives to delegate your .NET 6 implementation to Random.Shared, keeping the call-site cleaner.

Summary

In this post I described some of the thread-safety concerns and issues around the Random type, in various versions of .NET Framework and .NET Core. If you need thread-safe access to a Random instance in .NET 6, then you can use the built-in Random.Shared< /code> instance. However, if you're using earlier versions of .NET Core, you need to make sure you don't access the instance concurrently. And if you're using .NET Framework, you need to ensure you don't initialize the Random instances concurrently, but instead use a correctly random seed value.

Namaste Devops is a one stop solution view, read and learn Devops Articles selected from worlds Top Devops content publishers inclusing AWS, Azure and others. All the credit/appreciations/issues apart from the Clean UI and faster loading time goes to original author.

Comments

Did you find the article or blog useful? Please share this among your dev friends or network.

An android app or website on your mind?

We build blazing fast Rest APIs and web-apps and love to discuss and develop on great product ideas over a Google meet call. Let's connect for a free consultation or project development.

Contact Us

Popular DevOps Categories

Docker aws cdk application load balancer AWS CDK Application security AWS CDK application Application Load Balancers with DevOps Guru Auto scale group Automation Autoscale EC2 Autoscale VPC Autoscaling AWS Azure DevOps Big Data BigQuery CAMS DevOps Containers Data Observability Frequently Asked Devops Questions in Interviews GCP Large Table Export GCP Serverless Dataproc DB Export GTmetrix Page Speed 100% Google Page Speed 100% Healthy CI/CD Pipelines How to use AWS Developer Tools IDL web services Infrastructure as code Istio App Deploy Istio Gateways Istio Installation Istio Official Docs Istio Service Istio Traffic Management Java Database Export with GCP Jenkin K8 Kubernetes Large DB Export GCP Linux MSSQL March announcement MySQL Networking Popular DevOps Tools PostgreSQL Puppet Python Database Export with GCP Python GCP Large Table Export Python GCP Serverless Dataproc DB Export Python Postgres DB Export to BigQuery Sprint Top 100 Devops Questions TypeScript Client Generator anti-patterns of DevOps application performance monitoring (APM) aws amplify deploy blazor webassembly aws cdk application load balancer security group aws cdk construct example aws cdk l2 constructs aws cdk web application firewall aws codeguru reviewer cli command aws devops guru performance management aws service catalog best practices aws service catalog ci/cd aws service catalog examples azure Devops use cases azure devops whitepaper codeguru aws cli deploy asp.net core blazor webassembly devops guru for rds devops guru rds performance devops project explanation devops project ideas devops real time examples devops real time scenarios devops whitepaper aws docker-compose.yml health aware ci/cd pipeline example host and deploy asp.net core blazor webassembly on AWS scalable and secure CI/CD pipelines security vulnerabilities ci cd pipeline security vulnerabilities ci cd pipeline aws smithy code generation smithy server generator
Show more