[翻译].NET随机数

原文链接:http://csharpindepth.com/Articles/Chapter12/Random.aspx

 

随机数

当你在Stack Overflow上看到看到某个问题标题当中有“随机”这个词,你几乎能够肯定这和其他很多问题类似的基础的问题。这篇文章讲述了为什么随机这个概念引起了这么多的问题,以及如何去解决它们。

问题

Stack Overflow上的问题通常是这样的:

我使用Random.Next去产生随机数,但是方法一直返回同一个值。每一次跑这个随机数都会改变,但是这个方法会产生很多相同的随机数。

代码如下:

// Bad code! Do not use!
for (int i = 0; i < 100; i++)
{
    Console.WriteLine(GenerateDigit());
}
...
static int GenerateDigit()
{
    Random rng = new Random();
    // Assume there'd be more logic here really
    return rng.Next(10);
}

这到底发生了什么?

解释

Random类并不是一个真正的随机数生成器,而是一个伪随机数生成器。任何一个Random实例都有一定数量的状态值,当你调用Next方法时,实例会用这些状态值给你返回一些看上去是随机的数据。然后将内部的状态进行改变,然后下一次你就能够拿到下一组看上去随机的数。
所有的这些都是已经决定了的。如果你用同一个初始的状态值(可以通过种子来提供)去创建Random实例,然后调用这个实例的同样的方法,那么你将会拿到同样的数据。
那么,我们的演示代码到底哪里错了?我们在每一个循环中都创建一个新的Random实例。Random默认的构造函数会拿当前的日期和时间当作种子,在内部的当前日期和时间改变之前,你一般已经执行了很多代码了。所以我们一直重复地在使用同一个种子(译者注:因为Random默认的种子是最后一个计算机启动到目前的毫秒数,是毫秒级别的,所以多次调用情况下很容易出现同个种子的情况),然后重复地取到相同的结果。

我们该怎么办?

对于这个问题我们有很多的解决方案 - 有一些解决方案会优于其他的。让我们首先来看看其中的一个解决方案,这个方案和其他的都不一样。

使用加密随机数生成器

.NET框架有一个RandomNumberGenerator类,这是一个抽象类,所有的加密随机数生成器都必须继承自这个类。框架本身提供了一个派生类:RNGCryptoServiceProvider。加密随机数生成器的主要思想是,即使它是一个伪随机数的生成器,但是它做了很多工作是的其产生的随机数无法预测。内置的实现使用了很多能够有效代表你计算机的“噪声”的熵值,这就让随机数变得无法预测。(译者注:这里说的噪声可能是用户的鼠标轨迹、用户按键盘的位置等无法预测的东西)“噪声”可能不仅仅被用来产生一个种子,也可能在生成下一个随机数的时候被使用,所以即使你知道了当前的状态,你还是不足以去预测下一个结果。Windows系统还可以使用特定硬件上的随机性(比如说某个可以监测放射性同位素衰变的硬件)来确保随机数生成器更加的安全。

我们再拿Random类跟加密随机数生成器相比,如果你有10个Random.Next(100)返回的结果,并且你有足够的计算能力的话,你可能可以破解出原始的种子的值从而预测出下一个随机值。而且之前产生的随机值也可以得到。如果这些机制被用在一些安全或者金融用途上,这将可能是灾难性的。加密随机数生成器一般来说会比Random要慢,但是在产生独立无法预测的随机数这点上要比Random好得多。

很多情况下随机数生成器的效率并不是问题,友好的接口(API)才是问题。RandomNumberGenerator类被设计成生成随机的字节,而且只能以字节的形式。再看看Random类提供的接口则要友好许多,它允许生成一个随机的整数,随机的浮点数,或一些随机的字节。在使用RandomNumberGenerator时,我常常会发现我需要在一个区间内产生一个随机值,然后想要从一个随机的字节数组中可靠一致地获得这么一个随机值是很困难的。不是说不可能,但是你至少需要在RandomNumberGenerator上面在封装一层适配器类(Adapter Class)。在多数情况下,Random所产生的伪随机数就已经够用了,不过你要小心不要掉入前面提到的一些“陷阱”。下面就让我们看看如何才能避免“陷阱”。

重复使用单个Random实例

修复“很多重复随机数”的关键是重复的使用同一个Random实例。这听上去挺简单的…举个例子,我们可以将我们原来的代码改成这样:

// Somewhat better code...
Random rng = new Random();
for (int i = 0; i < 100; i++)
{
    Console.WriteLine(GenerateDigit(rng));
}
...
static int GenerateDigit(Random rng)
{
    // Assume there'd be more logic here really
    return rng.Next(10);
}

现在我们的循环会打印出不同的数字,但是我们还没有完成。如果你在短时间内多次调用这段代码的话会发生什么事情呢?我们可能还是会构造出两个拥有同样种子的Random实例。虽然说输出的数字都是不一样的,但是我们很容易得到两次一样的输出。

我们有两种方式避免这个问题。第一个方案是使用静态的字段去储存Random实例,并且在各处都是用这个实例。另一种方案是我们将Random的构造点放在一个更高层次的地方,当然这样子最终你会到达程序的入口。这样子我们就只会创建一个Random实例,然后将其传到各个需要使用的地方。这是一个不错的注意(而且能够很好的表达出依赖关系),但是这个方案并不能完整的工作,如果你代码使用多个线程的情况下这就会出问题。

线程安全问题

Random并非线程安全的。这确实是一个很大的硬伤,我们理想的情况是在程序里只存在一个实例并且在各个地方使用它。但是这样子是不行的!如果你在多个线程同时使用同一个实例的话,它很可能会将其内部的状态都变成0,这样子的话我们的Random实例就没有用了。

同样的,解决这个问题有两个方案。一个是仍然使用一个实例,但是利用加锁的方法来确保同时只有一个线程调用,每一个调用的地方都必须获得锁以后才能使用随机数生成器。我们可以封装一层使得调用者简单地使用。但是如果是在一个频繁使用多线程的系统里,这样很可能会浪费很多时间在等待锁上。

另外一个方案 - 一个线程只使用一个实例。我们需要做的是确保我们创建的实例不会重用同一个种子(也就是说我们不能直接调用默认的无参构造函数),这个方案相对来说还是比较简单明了的。

安全提供者

幸运的是,.NET 4中新增的ThreadLocal<T>类可以帮助我们非常方便的写出一个提供者来确保每个线程都有一个单一的实例。你只需要简单的提供给ThreadLocal<T>的构造函数一个委托让它去通过这个委托去获取一个T的实例,然后接下来的事情就是.NET框架帮你做了。在我的这个例子里,我选择使用一个种子变量,将其初始化值设成 Environment.TickCount(和默认的无参构造函数是一样的),然后每次调用完以后自增,这样子就可以保证每个线程都使用不同的种子。

看如下代码,这个类是一个静态类,只有一个公有的方法:GetThreadRandom。把它设计成一个方法而不是直接暴露属性主要是为了方便,这样子需要产生随机数的地方只需要引用Random类本身而不需要去引用Func<Random>。如果类型设计的时候只是用于单线程操作的话,Provider会调用委托来获得实例然后之后就会重用这个实例。如果Provider被多个线程使用的话,它每次都会去会调用委托来获得实例。ThreadLocal只会为每个线程创建一个实例,并且每个Random实例都会是用一个不同的种子。当需方法其传到依赖的是,我们可以是用一个方法的转换:new TypeThatNeedsRandom(RandomProvider.GetThreadRandom)

代码如下:

using System;
using System.Threading;

public static class RandomProvider
{    
    private static int seed = Environment.TickCount;

    private static ThreadLocal randomWrapper = new ThreadLocal(() =>
        new Random(Interlocked.Increment(ref seed))
    );

    public static Random GetThreadRandom()
    {
        return randomWrapper.Value;
    }
}

很简单是不是?这是因为这个类所关注的仅仅是提供正确的Random实例,它并不关心你拿到实例以后调用了什么方法或者是做了什么事情。当然这个类也有可能被误用,比如说拿到一个Random实例以后将其用到多线程的环境中,这个我们无法避免,但是这个类让我们更加容易的去做正确的事情。

接口(Interface)设计的问题

仍然有一个问题:它还是不够安全。就像我之前所说的,框架确实有更加安全的版本:RandomNumberGenerator,最常用的衍生类是RNGCryptoServiceProvider。但是,它提供的API在一般的场景下确实是非常之难用。

如果框架提供者能够区分出“随机源”的概念和“我要简单的获得一个随机数”的概念,我会很高兴。这样子我们就可以调用简单的API来生成随机数,然后再根据需求来选择是否需要使用安全的随机数源。但是,很遗憾,现实不是这样子的,或许在未来的版本里,又或许某一个第三方类库会提供一个适配器来取代之。(很遗憾,这已经超越我的能力了,最类似的这种事情是非常之困难的)。你可以通过继承Random然后重载SampleNextBytes方法来实现更安全的版本,但是这明显不是框架设计这应该去做的。

原文地址:https://www.cnblogs.com/imjustice/p/random_in_dotNet.html