当你在Stack Overflow网站标题中看到“随机”这个词你基本可以确定这是相同的基本问题无数的相似问题。本文带你探讨为什么随机性会引起这么多问题并且如何解决它们。
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); }
那么,这程序到底出了什么问题?
1.解读
这种Random类不是真正的随机数发生器,它是一个伪随机数发生器。任何Random实例都有一定量的状态,而当你调用Next( or NextDouble or NextBytes),它会使用该状态来返回到似乎是随机的数据,相应的改变它内部状态以便于在下一步调用时你将得到另一个伪随机数。
所有的这一切都是确定的,如果你开始一个Random的实例以相同的初始状态(可通过种子来提供),并使用相同的序列方法调用它,那你会得到相同的结果。
那么在我们的示例代码中到底出了什么问题? 我们使用的一个新的Random实例也在循环迭代。随机无参数的构造函数取当前日期和时间作为种子-在内部定时器工作之前你通常可以执行大量代码,当前的日期和时间就会发生变化。 因此,我们重复使用相同的种子就会重复得到相同的结果。
2.对此我们能做什么?
这个问题有很多的解决方案, 其中有些方法是比其他的更好。 让我们先挑出其中一种方法,因为它不同于其他的方法。
3.使用加密的随机数发生器
.NET有一个RandomNumberGenerator类应该是所有加密随机数生成器派生而来的抽象类。 这个框架本身附带了一个这样的派生类: RNGCryptoServiceProvider 。 加密随机数发生器的理念是,即使它可能仍然是一个伪随机生成器,它还是很难做到不可预料。 内置的实现需要多个熵源在你的电脑有效地呈现“噪音”,并难以预测。它可以使用这种噪音不仅仅是计算一个种子,也可以在生成下一个数字时让你知道当前的状态,这也许可能不足以预测下一个结果(或者那些已经生成),这主要取决于具体的实施。Windows也可以利用专业硬件资源的随机性(如一块硬件观察放射性同位素衰变),从而使得随机数发生器更加安全。
相比于这种随机,如果你看到(说)10个结果调用Random.Next(100)并投入大量计算资源任务,你可能会制定出最初的种子并预知接下来的结果将是...很有可能也会知道之前的结果是什么。 如果这种随机数应用于证券或金融的目的,这会是灾难性的事态。 加密随机数生成器通常比Random慢 ,但它在赋予数字难以预测和独立方面做得更好。
在很多情况下,随机数生成器的性能不是一个问题-但有一个适当的API就会出现问题。 随机数字生成器设计基础仅此是用来生成随机字节。比较这种API的随机 ,它可以让你请求一个随机整数,或随机double,或一组随机字节。我经常发现我需要一个整数的范围,得到可靠且一致地随机字节数组是很重要的。这不是不可能,但至少你可能会想要一个适配器类在随机数字生成器上。大多情况下,如果你能避免前面所述的陷阱,伪随机性的Random是可以接受的。
让我们看看如何能做到这一点。
4.用一个复用的实例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被每一个对象使用。另外,我们可以推高实例,当然是最终达到计划时,这永远只能实例化一个单一的元素随机性 ,并将其传递到任意地方。这是一个不错的主意(和它所表达的依赖性很好),但它不会完全的工作......至少,如果你的代码使用多个线程它会引发问题。
5.线程安全
Random不是线程安全的。这是一个真正的痛处,因为考虑到我们观念上是想在任何程序中如何使用单个实例。 但事实是,如果你从多个线程使用相同实例,它很可能以全零内部状态结束,此时该实例变得无用。
再次,在这里有两种方法可以解决这个问题。其一是仍然使用一个实例, 而且使用的每个调用方必须记住他们所使用的随机数生成器,同时获得锁。通过使用一个包装器锁定你就可以达到简化的效果,但在一个高度多线程系统中你仍然有可能浪费大量的时间等待加锁。
在这里我们将学会另一种方法 - 是让每个线程有一个实例。 我们需要确保,当我们创建实例时我们不要重复使用相同的种子(例如,所以我们不能只调用无参数的构造函数),但除此之外它是相对简单的。
6.一个安全驱动
很幸运的是,新ThreadLocal<T> .NET4类使得它很容易在每个线程需要单个实例中编写提供者。 您只需给ThreadLocal<T>构造一个委托调用来获得初始值当你不在的时候。 就我而言,我选择使用一个单一的种子变量,初始化使用Environment.TickCount(就像参数的Random构造函数),然后每递增,我们需要一个新的随机数生成器的时间-这是每一次的线程。
整个类是静态的,只有一种公开方法: 随机获得线程 。这是一个方法而不是一个属性大多为方便起见:而不是让其中需要随机数的类依赖于Random本身,他们会依赖于Func<Random> 。 如果这类型仅设计在单个线程中运行,它可以调用委托获得的单个实例Random和重复使用; 假如它能够从多个线程中每次使用调用委托它就需要一个随机数发生器。 这将只会创造尽可能多的实例有线程,每个将使用不同的种子开始。 在依赖传球的时候,我们就可以用一个方法转换:
new TypeThatNeedsRandom(RandomProvider.GetThreadRandom) 下面的代码:
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; } }
很简单,不是吗? 这是因为它的所有关注的是提供正确的Random实例 。 它并不在乎你采用什么样方法调用已经获取的实例。 代码仍然可以滥用这个类,当然,通过存放一个随机引用并用多个线程重复使用它,但要做对的事还是很容易的。
7.界面设计问题
一个问题仍然存在:这依旧不是很安全的。 正如我前面提到的,最常用的派生类是RNGCryptoServiceProvider,还有一个更安全随机数字发生器的版本,然而这个API在一般情况下还是很难使用。
假如框架驱动已经从“我想以简单的方法得到一个随机值”的概念中分离概念的“随机性源”,这确实是令人非常愉快的。 然后我们可以根据需要使用一个简单的API来支持一个安全的或不安全的随机源,很不幸的是,还没有这样的方法。也许在将来的迭代中......或者有个第三方会想出一个适配器来代替。(可惜这在我能力之上,很好地做好这件事情是相当困难的。)你几乎可以轻松成功地派生随机和覆盖示例及下个字节 ......但目前还不清楚他们需要如何工作,甚至Sample可能会非常棘手。 也许下一次...
这是一篇国外的文章,被我翻译过来。原文地址:http://csharpindepth.com/Articles/Chapter12/Random.aspx
接受批评指正,拒绝无脑喷粪。