随机生成数字1超过90%并行

时间:2016-02-29 14:10:12

标签: c# .net random parallel-processing

考虑以下计划:

public class Program
{
     private static Random _rnd = new Random();
     private static readonly int ITERATIONS = 5000000;
     private static readonly int RANDOM_MAX = 101;

     public static void Main(string[] args)
     {
          ConcurrentDictionary<int,int> dic = new ConcurrentDictionary<int,int>();

          Parallel.For(0, ITERATIONS, _ => dic.AddOrUpdate(_rnd.Next(1, RANDOM_MAX), 1, (k, v) => v + 1));

          foreach(var kv in dic)
             Console.WriteLine("{0} -> {1:0.00}%", kv.Key, ((double)kv.Value / ITERATIONS) * 100);
     }
}

这将打印以下输出:

请注意,每次执行时输出会有所不同

> 1 -> 97,38%
> 2 -> 0,03%
> 3 -> 0,03%
> 4 -> 0,03%
...
> 99 -> 0,03%
> 100 -> 0,03%

为什么数字1以这样的频率生成?

4 个答案:

答案 0 :(得分:21)

Random 线程安全。

Next没有什么特别的,以确保线程安全。

不要像这样使用Random。并且不要考虑使用线程本地存储持续时间,否则您将搞乱生成器的统计属性:您必须仅使用一个Random实例。一种方法是使用lock(_global)并在该锁定区域中绘制一个数字。

认为这里发生的事情是,到达生成器的第一个线程获得正确生成的随机数,并且所有后续线程都为每个绘图接收0。使用32个线程的“并行化”线程池,您在上面引用的比率大致得以实现;假设31个线程的结果放在第一个桶中。

答案 1 :(得分:6)

离线程本地存储解决方案更进一步,并试图避免统计问题,我建议使用从RNGCryptoServiceProvider生成的随机种子:

using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication1
{
    class Program
    {

        private static readonly int ITERATIONS = 5000000;
        private static readonly int RANDOM_MAX = 101;

        private static int GetCriptoRandom()
        {
            using (var rng = new System.Security.Cryptography.RNGCryptoServiceProvider())
            {
                byte[] bytes = new byte[4];
                rng.GetBytes(bytes);
                return BitConverter.ToInt32(bytes, 0);
            }
        }

        private static ThreadLocal<Random> m_rnd = new ThreadLocal<Random>(() => new Random(GetCryptoRandom()));

        private static Random _rnd
        {
            get
            {
                return m_rnd.Value;
            }
        }

        static void Main(string[] args)
        {
            ConcurrentDictionary<int, int> dic = new ConcurrentDictionary<int, int>();
            Parallel.For(1, ITERATIONS, _ => dic.AddOrUpdate(_rnd.Next(1, RANDOM_MAX), 1, (k, v) => v + 1));
            foreach (var kv in dic)
                Console.WriteLine("{0} -> {1:0.00}%", kv.Key, ((double)kv.Value / ITERATIONS) * 100);

        }
    }
}

从统计学角度看,结果范围为0.99%至1.01%。

答案 2 :(得分:1)

好吧,Random类不是线程安全一个,最简单的方法就是让它线程本地(每个线程都有自己的 Random实例):

private static ThreadLocal<Random> m_rnd = new ThreadLocal<Random>(() => new Random());

private static Random _rnd {
  get {
    return m_rnd.Value;
  }
}

https://msdn.microsoft.com/en-us/library/system.random(v=vs.110).aspx#ThreadSafety

答案 3 :(得分:1)

Random不是线程安全的 - 如果没有同步,则不得在多个线程中使用相同的Random实例。

为什么你特别得到1?那么,Random的工作方式(在4.5.2中)是通过保持种子数组和两个索引器。当您同时从多个线程使用它时,您的种子阵列将全部搞砸,并且您几乎总是在多个槽中获得相同的值。基本操作执行类似seed[a] - seed[b]的操作,当这些值相同时,您将返回零。既然你要求1作为最小值,那么这个零点会转移到1 - 这就是你的异常现象。这种情况在多核环境中发生得非常快,因为在每次Next呼叫中都有相当多的相互依赖状态。

有很多方法可以解决这个问题。一种是同步对一个公共Random实例的访问 - 只有在你做相对较少的random时才有意义,但在这种情况下你不会使用Parallel。如果性能是一个问题,你需要添加某种形式的预取(例如批量准备随机数,每个线程或使用一些并发队列),或使用其他方法。

另一种方法是为每个线程保留一个单独的Random实例。这需要您仔细选择每个实例的种子,否则您的随机数可能最终非常随机。 .NET本身使用的方法(同样,使用4.5.2代码作为参考)是使用Thread.CurrentThread.ManagedThreadId作为种子,这非常有效。另一种常见方法是使用单个全局(同步)Random实例来初始化其他Random的种子,但根据您的要求,您可能需要确保不会生成重复的种子。 / p>

当然,您也可以使用其他一些随机数生成器。然而,伪随机生成器通常需要与Random相同的方法 - 它们在很大程度上取决于它们的状态;这就是首先使它们伪随机的原因。加密生成器可能工作得更好,但那些往往非常慢,并且可能会回退到同步方法,尤其是在没有硬件支持的情况下。

在某些情况下,根据一些合理的规则分配生成工作是有意义的。例如,如果您对游戏内资产使用伪随机程序生成,那么以可重复的方式制定关于不同生成器种子的明确规则可能是有意义的 - 当然,这也意味着您无法真正使用{ {1}}或者,你必须更加明确。