【问题标题】:Random generates number 1 more than 90% of times in parallel [duplicate]随机生成数字 1 的次数超过 90% [重复]
【发布时间】:2016-06-12 14:52:39
【问题描述】:

考虑以下程序:

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?

【问题讨论】:

  • 并行真的相关吗?你确定没有它就不会发生这种情况吗?
  • Random 不是线程安全的。
  • @roryap 正常的 for 循环不会发生这种情况。至少在我的机器上。
  • Random 类不是线程安全的。您不应该在没有同步的情况下在多个线程中使用它。如果你这样做了,你应该期待一些奇怪的事情。
  • @MatiasCicero -- 好的。您可能想在您的问题中澄清这一点。

标签: c# .net random parallel-processing


【解决方案1】:

Random 不是线程安全的。

Next 没有做任何特别的事情来确保线程安全。

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

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

【讨论】:

  • 我明白了!我应该使用锁或类似的东西吗?
  • 我删除了我的 dv。我认为这是答案的一半,但它仍然没有回答基本问题或帮助解释行为:为什么它大部分时间都产生 1?
  • 因为它们是并行执行的,当从随机生成器请求一个数字时,它会从预先计算的表中获取一个数字,如果您同时要求两个数字,生成器不知道它应该是另一个并返回与计算表上的索引相同的值,仍然没有改变。
  • 我会假设 Random 的一个实例也不是解决方案,因为如果它不是您所说的线程安全,那么对 next 的两个同时调用可能很容易返回相同的结果。
  • 确实如此。您只能在locked 区域内拨打Next
【解决方案2】:

从线程本地存储解决方案更进一步,并试图避免统计问题,我建议使用从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%。

【讨论】:

  • 这看起来很有希望,但是 OP(或您)应该通过顽固的统计随机性测试来运行它。
  • System.Security.Cryptography.RNGCryptoServiceProvider 返回IDisposable 所以你必须把它包装成using;生成器的另一个问题是 cryptographic random 非常慢。
  • @DmitryBychenko。感谢您指出了这一点。编辑以将RNGCryptServiceProvider 包含在using 块中。虽然速度很慢,但每个线程只调用一次,因此对性能的影响很小。
  • @DmitryBychenko 性能不一定是问题,因为它只用于其他Randoms 的种子。不过,两个Randoms 很可能会得到相同的种子。根据这些随机值的使用方式,这可能是一个主要问题或微不足道的问题。
  • @Luann。我不会说这很有可能。其实很罕见,两个连续种子相同的概率是1/2^32
【解决方案3】:

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

为什么你特别得到 1?好吧,Random 的工作方式(在 4.5.2 中)是保留一个种子数组和两个索引器。当您同时从多个线程使用它时,您的种子数组将会变得一团糟,并且您几乎总是会在多个插槽中获得相同的值。基本操作类似于seed[a] - seed[b],当这些值相同时,你会得到零。由于您要求至少为 1,因此该零被转换为 1 - 这就是您的异常情况。这在多核环境中发生得非常快,因为在每次 Next 调用时都会更新大量相互依赖的状态。

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

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

当然,您也可以使用其他一些随机数生成器。然而,伪随机生成器通常需要与Random 相同的方法——它们严重依赖于它们的状态;这就是使它们首先成为伪随机的原因。加密生成器可能工作得更好,但它们往往很慢,并且无论如何都可能回退到同步方法,尤其是在没有硬件支持的情况下。

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

【讨论】:

    猜你喜欢
    • 2017-07-25
    • 2017-05-28
    • 1970-01-01
    • 2013-01-02
    • 1970-01-01
    • 2017-04-10
    • 1970-01-01
    • 2014-03-11
    • 1970-01-01
    相关资源
    最近更新 更多