天天看点

如何在 .NET Framework 中实现一个线程安全的 System.Random 对象?

作者:opendotnet

System.Random

类表示伪随机数生成器,这是一种能够产生满足某些随机性统计要求的数字序列的算法。

var rand = new Random();              var i = rand.Next();              Console.WriteLine(i);           

如果要在多线程环境下使用上述代码:

Parallel.For(0, 10, x =>              {              var rand = new Random();              var i = rand.Next();              Console.WriteLine(i);              });           

在 .NET Framework 平台上,会产生相同的输出(即所有的随机结果都是相同的):

如何在 .NET Framework 中实现一个线程安全的 System.Random 对象?

如果目标平台是 .NET 6.0 及以上,可以用以下代码替代:

Parallel.For(0, 10, x =>              {              var rand = Random.Shared;              var i = rand.Next();              Console.WriteLine(i);              });           

此时,输出结果是符合预期的。

实际上,如果在 .NET 6.0 平台上即使每次都创建新的 Random 对象,输出结果也是符合预期的:

如何在 .NET Framework 中实现一个线程安全的 System.Random 对象?

为了可以在 .NET Framework 及 .NET 6.0 之前的平台上获取正确的输出, 可以先尝试在多个线程中共用一个 Random 对象:

Console.WriteLine("Dotnet Version: " + Environment.Version);              var rand = new Random();              Parallel.For(0, 10, x =>              {              var i = rand.Next();              Console.WriteLine(i);              });           

这种方式似乎可以,因为小规模测试获得了正确的结果:

如何在 .NET Framework 中实现一个线程安全的 System.Random 对象?

以下代码创建了 10 个并发,并在每个并发中尝试获取了 10000 个随机数。然后将结果为零的数据统计出来:

Console.WriteLine("Dotnet Version: " + Environment.Version);              var rand = new Random();              Parallel.For(0, 10, _ =>              {              var numbers = new int[10_000];              for (int i = 0; i < numbers.Length; ++i)              {              numbers[i] = rand.Next(); //获取 10000 个随机数,引发线程安全问题。              }                  var numZeros = numbers.Count(x => x == 0); // 统计异常数据              Console.WriteLine($"得到 {numZeros} 个为零的结果");              });           

运行结果如下:

如何在 .NET Framework 中实现一个线程安全的 System.Random 对象?

这表明:System.Random 在高并发下确实出现了异常。如果使用了 Release 发布,则异常结果会更多。

如果我们愿意用自己的类型来包装

Random

,我们可以创建一个更好的解决方案。在下面的示例中,我们使用

[ThreadStatic]

为每个线程创建一个

Random

实例。这意味着我们重复使用

Random

实例,但是所有的访问总是来自单线程,因此保证了线程安全。这样我们最多创建

n

个实例,其中

n

是线程数。

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

使用方式如下:

Console.WriteLine("Dotnet Version: " + Environment.Version);              Parallel.For(0, 10, _ =>              {              var numbers = new int[10_000];              for (int i = 0; i < numbers.Length; ++i)              {              numbers[i] = ThreadLocalRandom.Instance.Next(); //获取 10000 个随机数,引发线程安全问题。              }                  var numZeros = numbers.Count(x => x == 0); // 统计异常数据              Console.WriteLine($"得到 {numZeros} 个为零的结果");              });           

请注意,在这个简单的示例中,如果在线程之间传递

ThreadLocalRandom.Instance

,仍然有可能遇到线程安全问题。例如,下面显示了与之前相同的问题:

Console.WriteLine("Dotnet Version: " + Environment.Version);              var rand = ThreadLocalRandom.Instance;              Parallel.For(0, 10, _ =>              {              var numbers = new int[10_000];              for (int i = 0; i < numbers.Length; ++i)              {              numbers[i] = rand.Next(); //获取 10000 个随机数,引发线程安全问题。              }                  var numZeros = numbers.Count(x => x == 0); // 统计异常数据              Console.WriteLine($"得到 {numZeros} 个为零的结果");              });           

一个简单的方案就是隐藏 Random 实例,仅暴露 Next 方法:

using System;                  internal static class ThreadSafeRandom              {              [ThreadStatic]              private static Random _local;                  private static Random Instance              {              get              {              if (_local is )              {              _local = new Random();              }                  return _local;              }              }                  public static int Next() => Instance.Next();              }           

但这仍然无法解决在 .NET Framework 上因为系统时钟的分辨率过低造成的重复值问题:

如何在 .NET Framework 中实现一个线程安全的 System.Random 对象?

可以看到 1147840056 重复出现了很多次。

要解决该问题,只要在创建

Random

对象时,指定不同的随机种子即可:

using System;                  internal static class ThreadSafeRandom              {              [ThreadStatic]              private static Random _local;              private static readonly Random Global = new Random(); // 全局实例,用于生成随机种子                  private static Random Instance              {              get              {              if (_local is )              {              int seed;              lock (Global) // 确保 Global 不会被并发访问              {              seed = Global.Next();              }                  _local = new Random(seed);              }                  return _local;              }              }                  public static int Next() => Instance.Next();              }           

如果您使用的是 .NET 6+ ,我仍然建议您使用内置的

Random.Shared

,但如果您没有那么幸运,您可以使用

ThreadSafeRandom

来解决您的问题。

如果您的目标是 .NET 6 和其他框架,您可以使用 #if 指令将您的 .NET 6 实现委托给

Random.Shared

,从而保持调用干净。