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

考虑以下程序:

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 dic = new ConcurrentDictionary(); 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以这样的频率生成?

Random 不是线程安全的。

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

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

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

RNGCryptoServiceProvider本地存储解决方案更进一步,并试图避免统计问题,我建议使用从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 m_rnd = new ThreadLocal(() => new Random(GetCryptoRandom())); private static Random _rnd { get { return m_rnd.Value; } } static void Main(string[] args) { ConcurrentDictionary dic = new ConcurrentDictionary(); 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%。

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

 private static ThreadLocal m_rnd = new ThreadLocal(() => 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

Random不是线程安全的 – 您不能在没有同步的情况下从多个线程使用相同的Random实例。

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

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

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

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

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