C#随机数生成函数Random介绍

生成随机数在很多编程环境中都需要用到,这个看似简单的操作却很容易发生问题,当你在网上搜索时很快就会发现很多同行有相同的经历。

本文将介绍C#中随机数生成函数Random()原理及使用。

常见问题
-

使用Random.Next生成随机数时,往往会遇到多次运行得到的确实同一个值的情况。

这个问题可能由以下类似代码造成:

    //Bad code! Do not use!
    for(int i=0; i<100; i++)
    {
        Console.WriteLine(GenerateDigit());
    }

    static int GenerateDigit()
    {
        Random rand = new Random();
        return rand.Next(10);
    }

那么,上述代码错在哪里呢?

解析
-

实际上,Random类并不是一个真正意义上的随机数生成器,它是伪随机的。任意一个Random实例都有一个确定的状态,当你调用Next(或NextDouble、NextBytes)会根据该状态返回一个随机数,当这个内部的状态改变时,下一次Next函数的的调用就会产生另外一个随机数。

若某一序列方法调用的是同一状态(种子seed)的Random实例,则会产生相同的数值。

那么在上述代码中是哪里错了呢? 原因在于,我们在for循环中使用了一个无参数的Random构造函数,则Random会使用当前的datatime来作为种子。而一般来说我们在for循环内的若干代码可以在CPU的一个内部时间片内执行完毕,因此当前的datetime并没有变化。所以当下一次循环执行时还是使用的与上一次循环相同Random种子,从而导致输出同一个结果。

解决方案
-

既然知道导致问题的原因,那么解决起来就比较简单了,网上有很多解决方案:

  • 使用 RNGCryptoServiceProvider类
  • 重复使用同一个Random实例

第一种基于密码学的随机数生成器的介绍这里不表,有兴趣的同学可以参考MSDN.aspx).

这里主要介绍下第二种方式:

重复使用同一个Random实例
-

上述示例代码只要稍微修改下即可符合使用同一个Random实例的特点,改进后的代码如下:

    //better code.
    Random rand = new Random();
    for(int i=0; i<100; i++)
    {
        Console.WriteLine(GenerateDigit());
    }

    static int GenerateDigit()
    {
        return rand.Next(10);
    }

现在我们的代码在循环时,确实能够打印出不同的数值了,不过再想想是不是真的就OK了?

比如:如果外部函数在一个很短的时间内多次调用了上述代码段,那么Random实例化时很可能就使用同一个时间点作为种子,那么这些调用产生的所有数值虽然不会相同,但也很可能出现多个重复数值。

此时,有两种方式可以避免:

1)将Random实例设为Static,这样保证类只有一个Random实例;

2)将Random的实例提前,如果将Random的实例放置程序的开始处,后续的调用都传递当前引用。

上述两种方法都较为容易实现,但还是推荐使用Static方式,这样让编译器来保证只有一个实例总比人为的copy要轻松。

    // More better code.
    public class RandomGenerator{
        static Random random = new Random();

        public static int GenerateDigit(){
            return random.Next(10);
        }

    }

真的完美了吗? 线程安全吗?
-

static版的代码看起来很理想,然而Random是非线程安全的,在多线程程序下,可能会发生多个线程同时使用该Random实例的情况,这时这些Random类将会返回0.

对Random和线程安全的议题,MSDN上有较为详细的介绍和例子:System.Random.aspx).

其大致的解决方案都是使用线程的同步对象来确保每个线程调用时都是独占的。

总结与建议
-

不管怎样,推荐的做法是在一个程序里面使用同一个Random实例(如static版本),若需要在多线程下使用,可在调用RandomGenerator的应用类处加上lock语句,以此来保证线程安全。当然,lock操作必然会增加程序的开销,影响程序性能。

因此,具体怎样使用Random,取决于程序的使用场景,如果不是重度的多线程程序,可以不加锁;如果是多线程比较频繁的系统,那么就用加锁保证线程安全。

参考:

C# in Depth/Random.

标签: c#, random, 随机数, 随机数生成器

添加新评论