zoukankan      html  css  js  c++  java
  • C# Random类的正确应用

    Random类介绍

    Random类一个用于产生伪随机数字的类。这里的伪随机表示有随机性但是可以基于算法模拟出随机规律。
    Random类的构造方式有两种。

    • Random r= new Random()。会以当前系统时间作为默认种子构建一个随机序列
    • Random r = new Random(unchecked((int)DateTime.Now.Ticks));。自定义一个种子,通常会使用时间Ticks。

    随机性保证

    由于Random的伪随机性,所以如果多个Random随机序列生成的时间间隔很短(官方说法15ms内),那么他们产生的随机数会大概率相同。如下列代码

        /// <summary>
        /// 错误的Random构建。
        /// </summary>
        public static void Bad_Random()
        {
            //正确做法应当将 Random构建防止循环外。
            //Random创建间隔时间极短的情况下,随机算法序列会基本一致,倒是随机性也是一致的
            //var r = new Random();
            for (int i = 0; i < 10; i++)
            {
                var r = new Random();
                var val = r.Next(1, 100);
                Console.WriteLine(val);
            }
        }
    

    运行结果:

    所以在生产中通常可以考虑将Random单例化,以保证其随机算法的序列独一性。这也是官方推荐的方式。

    Instead of instantiating individual Random objects, we recommend that you create a single Random instance to generate all the random numbers needed by your app. 
    

    这个问题在.net core下官方组件已对Random的构建作优化,所以上面的案例代码如果放在.net core项目下运行,你会发现可以正确的生成随机数。有兴趣的小伙伴可以自己尝试一下。不过为了代码的延续性,还是建议Random作为单例模式设计。

    那么将Random设计为单例是否就解决了随机性的问题了呢,这时候就涉及到另外一个问题,Random不是线程安全的。如下列代码

        /// <summary>
        /// 生成一个10位随机数
        /// 设定了一定的复杂性,保证单线程下随机数不重复
        /// </summary>
        /// <param name="random">Random.</param>
        /// <returns>随机数.</returns>
        private static string GenerateRandomStr(Random random)
        {
            string source = "ABCDEFGHIKLMNOPQRTUVWXYZabcdefghiklmnopqrtuvwxyz";
            int length = 10;
            var list = Enumerable.Repeat(source, length)
                 .Select(s => s[random.Next(s.Length)]).ToArray();
            return new string(list);
        }
        /// <summary>
        /// 单线程基本可以保证唯一性
        /// </summary>
        public static void Good_Random_In_SingleThread()
        {
            //正确做法应当将 Random构建防止循环外。
            //Random创建间隔时间极短的情况下,随机算法序列会基本一致,倒是随机性也是一致的
            var r = new Random();
            ConcurrentBag<string> list = new ConcurrentBag<string>();
            for (int i = 0; i < 20000; i++)
            {
                var val = GenerateRandomStr(r);
                list.Add(val);
            }
    
            Console.WriteLine($"单线程下重复数据有:{20000 - list.Distinct().Count()}");
        }
    
        /// <summary>
        /// 多线程下的Random构建。
        /// Bad案例,Random非线程安全
        /// 多线程高并发情况下,会出现概率重复
        /// </summary>
        public static void Bad_Random_In_MultThreads()
        {
            var r = new Random(unchecked((int)DateTime.Now.Ticks));
            ConcurrentBag<string> list = new ConcurrentBag<string>();
    
            var t1 = Task.Run(() =>
            {
                for (int i = 0; i < 10000; i++)
                {
                    var val = GenerateRandomStr(r);
                    list.Add(val);
                }
            });
    
            var t2 = Task.Run(() =>
            {
                for (int i = 0; i < 10000; i++)
                {
                    var val = GenerateRandomStr(r);
                    list.Add(val);
                }
            });
    
            Task.WaitAll(t1, t2);
    
            Console.WriteLine($"线程1和线程2的重复数据有:{20000 - list.Distinct().Count()}");
        }
    

    运行结果:

    这种重复率在生产环境上是不可接受的。那么产生的原因是什么呢?根源还是在伪随机线程不安全上。我们可以想象下,一个Random实例中基于随机算法产生的一个随机数序列,在单线程下pop出一个随机数,然后指向下一个随机数。而在高并发的多线程情况下,指向下一个随机数的动作还未完成时,另一个线程又来请求pop,这样相同的随机数被重复pop了。

    网上有很多多线程下Random的解决方案,我查阅了一些感觉都不是很好。以下是我的解决方案。用到了 ThreadLocal。这个类详细的作用大家可以自己去查阅,这里大家只需要知道这个类可以保证它包含的对象只能线程内独享。简单说,同一类型对象 每个线程都独有一个Random实例互不影响。

        //利用ThreadLocal 实现每个线程下Random独有
        //再通过seed原子性变更,保证每个Random的seed不同而生成的随机数列也不同
        private static int seed = 100;
        private static ThreadLocal<Random> threadLocal = new ThreadLocal<Random>(() => new Random(Interlocked.Increment(ref seed)));
    
        /// <summary>
        /// 多线程下的Random构建。
        /// </summary>
        public static void Good_Random_In_MultThreads()
        {
            ConcurrentBag<string> list = new ConcurrentBag<string>();
    
            var t1 = Task.Run(() =>
            {
                for (int i = 0; i < 10000; i++)
                {
                    var val = GenerateRandomStr(threadLocal.Value);
                    list.Add(val);
                }
            });
    
            var t2 = Task.Run(() =>
            {
                for (int i = 0; i < 10000; i++)
                {
                    var val = GenerateRandomStr(threadLocal.Value);
                    list.Add(val);
                }
            });
    
            Task.WaitAll(t1, t2);
    
            Console.WriteLine($"[ThreadLocal模式]线程1和线程2的重复数据有:{20000 - list.Distinct().Count()}");
        }
    

    运行结果:

    由此可见,基于ThreadLocal的特性,并区别了每个线程下的seed都不一样,从而保证每个Random的随机性也不行一样。

    那么到这里Random的随机性问题解决了吗??
    再深入思考下,对于集群部署情况,多台服务器同时运行,上述的Random随机性能保证吗?聪明的小伙伴应该能想到在不同服务器上,由于初始seed相同,可能又导致Random的随机性相同的情况发生。

    那么解决方案也很简单,保证每台服务器的初始seed不同即可。这里的解决方案很多,不限于机器编号、IP地址后几位、启动时间(Environment.TickCount)等等。

    这样,到这里Random的随机性问题终于可以告一段落了。

  • 相关阅读:
    TCP中的三次握手与四次挥手
    C++中的访问控制与封装
    C++中的类定义
    Verilog学习笔记设计和验证篇(三)...............同步有限状态机的指导原则
    Verilog学习笔记简单功能实现(三)...............同步有限状态机
    Verilog学习笔记设计和验证篇(二)...............同步有限状态机
    Verilog学习笔记设计和验证篇(一)...............总线和流水线
    Verilog学习笔记简单功能实现(二)...............全加器
    Verilog HDL模型的不同抽象级别
    Verilog学习笔记简单功能实现(一)...............D触发器
  • 原文地址:https://www.cnblogs.com/gt1987/p/14034660.html
Copyright © 2011-2022 走看看