原文链接: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方法来实现更安全的版本,但是这明显不是框架设计这应该去做的。

[翻译].NET随机数的更多相关文章

  1. 百度翻译cs文件英文注释

    原由:本人英语烂,没办法看不懂国外的代码注释!只能借助其他手段来助我一臂之力了. 虽然翻译内容不是很准确,但好过什么都看不懂的强. 对吧?! 代码有点乱有用的园友自个整理一下吧! 最近没时间所以翻译后 ...

  2. 【翻译二十三】java-并发程序之随机数和参考资料与问题(本系列完)

    Concurrent Random Numbers In JDK 7, java.util.concurrent includes a convenience class, ThreadLocalRa ...

  3. 文献翻译|Design of True Random Number Generator Based on Multi-stage Feedback Ring Oscillator(基于多级反馈环形振荡器的真随机数发生器设计)

    基于多级反馈环形振荡器的真随机数发生器设计 摘要 真随机数生成器(trng)在加密系统中起着重要的作用.本文提出了一种在现场可编程门阵列(FPGA)上生成真随机数的新方法,该方法以 多级反馈环形振荡器 ...

  4. 机器指令翻译成 JavaScript —— No.7 过渡语言

    上一篇,我们决定使用 LLVM 来优化程序,并打算用 C 作为输入语言.现在我们来研究一下,将 6502 指令转换成 C 的可行性. 跳转支持 翻译成 C 语言,可比 JS 容易多了.因为 C 支持 ...

  5. 【原创】开源Math.NET基础数学类库使用(12)C#随机数扩展方法

                   本博客所有文章分类的总目录:[总目录]本博客博文总目录-实时更新  开源Math.NET基础数学类库使用总目录:[目录]开源Math.NET基础数学类库使用总目录 前言 ...

  6. Spark官方文档 - 中文翻译

    Spark官方文档 - 中文翻译 Spark版本:1.6.0 转载请注明出处:http://www.cnblogs.com/BYRans/ 1 概述(Overview) 2 引入Spark(Linki ...

  7. Manual——Test (翻译1)

    LTE Manual ——Logging(翻译) (本文为个人学习笔记,如有不当的地方,欢迎指正!) 1.17.3 Testing framework(测试框架)   ns-3 包含一个仿真核心引擎. ...

  8. HTTP认证机制(翻译)

    发现一篇介绍HTTP认证的好文章,就尝试翻译了一下,记录在下面.(翻译的很挫,哈哈哈) 原文: http://frontier.userland.com/stories/storyReader$215 ...

  9. 百度翻译&&金山词霸API

    #/usr/bin/env python3 #coding=utf8 """百度翻译api功能实现函数,本模块基于Python3.x实现,getTransResult(q ...

随机推荐

  1. MongoDB(NoSQL) 入门

    一.简介 NoSQL数据库因其可扩展性使其变得越来越流行,利用NoSQL数据库可以给你带来更多的好处, MongoDB是一个用C++编写的可度可扩展性的开源NoSQL数据库. 本文主要讲述MongoD ...

  2. PHP使用内置函数生成图片的方法详解

    原文地址:http://www.poluoluo.com/jzxy/201605/475301.html 本文实例讲述了PHP使用内置函数生成图片的方法.分享给大家供大家参考,具体如下: 第一步:创建 ...

  3. ready与onload的性能

    <!DOCTYPE html> <html> <head> <title>ready与onload的性能</title> <meta ...

  4. iOSview整体上移下移(点击键盘)

    首先创建一个textFiled 并实现起代理方法 - (void)textFieldDidBeginEditing:(UITextField *)textField { //设置动画的名字 [UIVi ...

  5. 记一次FTP上传文件总是超时的解决过程

    好久没写博,还是重拾记录一下吧. 背景:买了一个阿里云的云虚拟机用来搭建网站(起初不了解云虚拟主机和云服务器的区别,以为都是有SSH功能的,后来发现不是这样样子啊,云虚拟机就是FTP上传网页+MySQ ...

  6. js跳转到新页面传参以及接收参数的方法

    1.传递参数: window.location.href = "./list.html?id="+id; 1.接收参数: (1)接收参数函数封装 function GetReque ...

  7. mysql数据表操作&库操作

    首先登陆mysql:mysql -uroot -proot -P3306 -h127.0.0.1 查看所有的库:show databases; 进入一个库:use database; 显示所在的库:s ...

  8. php安装libiconv-1.14.tar.gz遇到的问题

    遇到的Error code In file included from progname.c:26:0: ./stdio.h:1010:1: error: ‘gets‘ undeclared here ...

  9. 【开发环境】JAVA 环境变量批处理

    @echo off set regpath=HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environmen ...

  10. css中关于position属性的探究(原创)

    关于position属性的设置,头脑中一直觉得不是很清楚,所以借助这次机会单独自己测试了一下,记作学习笔记.   首先,css的position属性包含下面四种设置情况: static:默认属性.指定 ...