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 6.0 及以上,可以用以下代码替代:
Parallel.For(0, 10, x => { var rand = Random.Shared; var i = rand.Next(); Console.WriteLine(i); });
此时,输出结果是符合预期的。
实际上,如果在 .NET 6.0 平台上即使每次都创建新的 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); });
这种方式似乎可以,因为小规模测试获得了正确的结果:
以下代码创建了 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} 个为零的结果"); });
运行结果如下:
这表明: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 null) { _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 null) { _local = new Random(); } return _local; } } public static int Next() => Instance.Next(); }
但这仍然无法解决在 .NET Framework 上因为系统时钟的分辨率过低造成的重复值问题:
可以看到 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 null) { 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
,从而保持调用干净。