C# 随机函数Random 半翻

当你在 Stack Overflow 看到有关Random标题的问题时,肯定知道这又是无数类似基本问题中的一个,下面这篇文章将讲解为随机性为何产生如此多的问题,以及如何解

问题

 Stack Overflow (or newsgroup, or mailing list etc) 中的相关问题内容基本上就是:

         "我在使用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(或者NextDouble或者NextBytes)的时候,它会根据当时特定状态产生数字,这些数字表面上看起来是随机的,伴随着内部状态的变化,当你再次调用Next方法是又会产生貌似随机的数字。

可以确认的是,如果创建的实例拥有相同的初始状态(可以通过seed提供)并且按照相同的顺序调用其方法,你将会得到相同的结果。

那这段代码的问题在哪里?每个循环里我们使用了新的random对象实例。Random类的无参构造函数使用当前日期和时间作为seed--所以在内部定时器计算出当前日期和时间已经变化之前,代码已经执行多次了。因此我们重复使用了相同的seed--其结果也是重复的。

怎么办?

有很多方案,下面一些方案可能会更好一些

使用基于密码安全的随机数产生器

.Net里面有个RandomNumberGenerator的抽象类,所有的基于密码安全的随机数产生器类都要继承它,.net framework 自己实现了一个RNGCryptoServiceProvider。基于密码安全随机数产生器的主要设计思想是,虽热它产生的随机数仍然是伪随机数,但它尽量保持不可预测。内部的实现算法使用多种很难预测的有效代表计算机噪声的编码源,它不只在计算seed的时候使用这些噪声参数,在产生随机数的时候也会使用到,所以,即便你知道当前状态,也很难预测下次(或者已经产生的)随机数的结果。取决于实现的方法,如windows可以使用指定的硬件信息(如硬件监控核同位素的衰败)来提高产生随机数的安全性。
       和Random相比。如果你观察10次Random.Next(100)结果,并且有相当多的计算资源,你可能计算出初始化的seed并能预测下次Next(100)的调用结果----和先前的结果一样,有这种可能。如果那个随机数是用作金融安全领域,这非常危险。基于密码安全的随机数产生器效率要比Random慢,但它会给你更难预测和无关联的随机数。.

     大多数情况下,随机数产生效率不是大问题 --问题是有没有合适的随机函数API。 RandomNumberGenerator 只是用来产生随机的bytes-仅此而已。Compare this with the API of Random, which lets you ask for a random integer, or a random double, or a random set of bytes. I usually find that I need an integer in a range - and getting that reliably and uniformly from a random array of bytes is non-trivial. It's far from impossible, but at the very least you'd probably want an adapter class on top of RandomNumberGenerator. In many cases, the pseudo-randomness of Random is acceptable - if you can avoid the pitfalls described earlier. Let's look at how we can do that.

重复使用同一个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);
}

现在每个循环会打印出不同的数字....但还没完。想想当你多次快速成功调用这个函数时会发生什么?我们仍然可能创建了两个使用相同seed的Random实例,那么相同的结果我们就会重现两次。

        有两种方法可以解决这个问题,一个选择就是使用静态变量维持一个Random的实例供每个用户调用。另外一个选择是(不知咋翻)There are two ways of avoiding this problem. One option is to use a static field to maintain a single instance of Random which everything uses. Alternatively, we could push the instantiation higher, of course - eventually reaching the top of the program, which only ever instantiates  a single element of Random, and passes it down everywhere. That's a nice idea (and it expresses the dependency nicely), but it won't quite work... at least, it could cause problems if your code uses multiple threads.

线程安全

Random isn't thread-safe. It's a real pain, given how we'd ideally like to use a single instance everywhere in our program. But no, if you use the same instance at the same time from multiple threads, it's quite possible to end up with a state of all zeroes internally, at which point the instance becomes useless.

Again, there are two approaches here. One is to still use one instance, but also use a lock which every caller has to remember to acquire while they're using the random number generator. That can be simplified by using a wrapper which does the locking for you, but in a heavily multithreaded system you'll still potentially waste a lot of time waiting for locks.

The other approach - and the one we'll pick up here - is to have one instance per thread. We need to make sure that when we create instances we don't reuse the same seed (so we can't just call the parameterless constructor, for example), but other than that it's relatively straightforward.

A safe provider

Fortunately, the new ThreadLocal<T> class in .NET 4 makes it very easy to write providers which need to have a single instance per thread. You simply give theThreadLocal<T> constructor a delegate to call to obtain the initial value, and you're away. In my case I've chosen to use a single seed variable, initialized usingEnvironment.TickCount (just like the parameterless Random constructor) which is then incremented every time we need a new random number generator - which is once per thread.

The whole class is static, with just a single public method: GetThreadRandom. It's a method rather than a property mostly for the sake of convenience: rather than make any classes which need random numbers depend on Random itself, they'll depend on Func<Random>. If the type is only designed to operate in a single thread, it can call the delegate to obtain a single instance of Random and reuse it; if it could be used from multiple threads it can call the delegate each time it needs a random number generator. This will only create as many instances as there are threads, and each will start with a different seed. When passing in the dependency, we can then use a method conversion: new TypeThatNeedsRandom(RandomProvider.GetThreadRandom). Here's the code:

using System;
using System.Threading;
 
public  static  class RandomProvider
{    
     private  static  int seed = Environment.TickCount;
    
     private  static ThreadLocal<Random> randomWrapper =  new ThreadLocal<Random>(() =>
         new Random(Interlocked.Increment( ref seed))
    );

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

Simple, isn't it? That's because all it's concerned with is providing the right instance of Random. It doesn't care what methods you call on the instance after it's been fetched. Code can still abuse this class of course, by stashing one Random reference and reusing it from multiple threads, but it's also easy to do the right thing.

Problems with interface design

One problem remains: this still isn't very secure. As I mentioned earlier, there's a more secure version in RandomNumberGenerator, with the most commonly used derived class being RNGCryptoServiceProvider. However, the API to that really is hard to use in common cases.

It would be very pleasant indeed if the framework providers had separated the "source of randomness" concept from the "I want to get a random value in a simple way" concept. Then we could have used a simple API backed by either a secure or insecure source of randomness, depending on our requirements. Unfortunately, 'twas not to be. Maybe in a future iteration... or maybe a third party will come up with an adapter instead. (It's beyond me, unfortunately... doing this kind of thing properly is remarkably difficult.) You can nearly get away with deriving from Random and overriding Sample and NextBytes... but it's not clear exactly how they need to work, and even Sample can be tricky. Maybe next time...

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值