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
,从而保持调用干净。