[翻译].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
然后重载Sample
和NextBytes
方法来实现更安全的版本,但是这明显不是框架设计这应该去做的。
[翻译].NET随机数的更多相关文章
- 百度翻译cs文件英文注释
原由:本人英语烂,没办法看不懂国外的代码注释!只能借助其他手段来助我一臂之力了. 虽然翻译内容不是很准确,但好过什么都看不懂的强. 对吧?! 代码有点乱有用的园友自个整理一下吧! 最近没时间所以翻译后 ...
- 【翻译二十三】java-并发程序之随机数和参考资料与问题(本系列完)
Concurrent Random Numbers In JDK 7, java.util.concurrent includes a convenience class, ThreadLocalRa ...
- 文献翻译|Design of True Random Number Generator Based on Multi-stage Feedback Ring Oscillator(基于多级反馈环形振荡器的真随机数发生器设计)
基于多级反馈环形振荡器的真随机数发生器设计 摘要 真随机数生成器(trng)在加密系统中起着重要的作用.本文提出了一种在现场可编程门阵列(FPGA)上生成真随机数的新方法,该方法以 多级反馈环形振荡器 ...
- 机器指令翻译成 JavaScript —— No.7 过渡语言
上一篇,我们决定使用 LLVM 来优化程序,并打算用 C 作为输入语言.现在我们来研究一下,将 6502 指令转换成 C 的可行性. 跳转支持 翻译成 C 语言,可比 JS 容易多了.因为 C 支持 ...
- 【原创】开源Math.NET基础数学类库使用(12)C#随机数扩展方法
本博客所有文章分类的总目录:[总目录]本博客博文总目录-实时更新 开源Math.NET基础数学类库使用总目录:[目录]开源Math.NET基础数学类库使用总目录 前言 ...
- Spark官方文档 - 中文翻译
Spark官方文档 - 中文翻译 Spark版本:1.6.0 转载请注明出处:http://www.cnblogs.com/BYRans/ 1 概述(Overview) 2 引入Spark(Linki ...
- Manual——Test (翻译1)
LTE Manual ——Logging(翻译) (本文为个人学习笔记,如有不当的地方,欢迎指正!) 1.17.3 Testing framework(测试框架) ns-3 包含一个仿真核心引擎. ...
- HTTP认证机制(翻译)
发现一篇介绍HTTP认证的好文章,就尝试翻译了一下,记录在下面.(翻译的很挫,哈哈哈) 原文: http://frontier.userland.com/stories/storyReader$215 ...
- 百度翻译&&金山词霸API
#/usr/bin/env python3 #coding=utf8 """百度翻译api功能实现函数,本模块基于Python3.x实现,getTransResult(q ...
随机推荐
- CSS里常见的块级元素和行内元素
根据CSS规范的规定,每一个网页元素都有一个display属性,用于确定该元素的类型,每一个元素都有默认的display属性值,比如div元素,它的默认display属性值为“block”,成为“块级 ...
- [学习笔记]坚果云网盘,SVN异地代码管理
SVN的好处不必多说了.但是如果希望有一份自己的用来学习和储备的代码仓库,那么能够异地同步是必不可少的了. 参考作者Mike_QSJ的文章,但是实际上做了很大的改动.一方面使用更常见的windows系 ...
- sql server 2000数据库 最近经常出现某进程一直占用资源,阻塞?死锁?
OA的数据库最近多次出现某进程一直占用资源,导致其他进程无法执行.使用sp_who2 和 sql server profiler跟踪查询,发现有以下几个语句常常占用资源: 1.declare @P1 ...
- 网站内容禁止复制和粘贴、另存为的js代码(转)
1.使右键和复制失效 方法1: 在网页中加入以下代码: 代码如下: <script language="Javascript"> document.oncontextm ...
- python基础第三天(1)
函数 函数分为:内置函数,自定义函数,导入函数. 内置函数 python为咱们提供的快捷方式 vars()---针对脚本的,找到这个脚本中的所有变量. #!/usr/bin/env python # ...
- load与initialize
NSObject类有两种初始化方式load和initialize load + (void)load; 对于加入运行期系统的类及分类,必定会调用此方法,且仅调用一次. iOS会在应用程序启动的时候调用 ...
- Only女装首页HTML+CSS代码实现
这是效果图,因为太长,缩略了. 在学习HTML和CSS基础的时候做的.自己切图下来做的. 没有什么技术含量. 源代码和图片我放在github上了, 上个链接吧: https://github.com/ ...
- Android修改Eclipse 中的Default debug keystore路径,以及修改android的AVD默认路径
初学android,光是配置Eclipse就走了不少弯路,班里面有很多同学的计算 机名都是写的自己的中文姓名,结果导致了AVD文件默认保存在“C:\user\<username>\.and ...
- 天气预报API(一):全国城市代码列表(“旧编码”)
说明 2016-12-09 补充 (后来)偶然发现中国天气网已经有城市ID列表的网页... 还发现城市编码有两种,暂且称中国天气网这些编码为旧标准 "旧编码"的特征是 9个字符长度 ...
- <Oracle Database>诊断文件
诊断文件 诊断文件是获取有关数据库活动的信息的一种方式,用于解决数据库出现的一些问题,主要包含有关数据库中出现的重要事件的一些信息,这些文件能更好的对数据库进行日常的管 理,主要类型有一下几种: 警告 ...