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的随机性问题终于可以告一段落了。

  • 相关阅读:
    前沿技术解密——VirtualDOM
    Ques核心思想——CSS Namespace
    Unix Pipes to Javascript Pipes
    Road to the future——伪MVVM库Q.js
    聊聊CSS postproccessors
    【译】十款性能最佳的压缩算法
    Kafka Streams开发入门(9)
    Kafka Streams开发入门(8)
    【译】Kafka Producer Sticky Partitioner
    【译】99th Percentile Latency at Scale with Apache Kafka
  • 原文地址:https://www.cnblogs.com/gt1987/p/14034660.html
Copyright © 2011-2022 走看看