2011-12-14 47 views
0

我很努力地找到一种方法,让每个线程有一个随机数生成器,同时确保重新运行程序时生成相同的数字。用已知种子创建ThreadLocal随机生成器

我现在做的是这样的:

class Program { 
    static void Main(string[] args) { 

     var seed = 10; 
     var data = new List<double>(); 
     var dataGenerator = new Random(seed); 

     for (int i = 0; i < 10000; i++) { 
      data.Add(dataGenerator.NextDouble()); 
     } 

     var results = new ConcurrentBag<double>(); 

     Parallel.ForEach(data, (d) => { 
      var result = Calculate(d, new Random(d.GetHashCode()); 
      results.Add(result); 
     }); 

    } 

    static double Calculate(double x, Random random) { 
     return x * random.NextDouble(); 
    } 
} 

因为创建了“数据”列表中的randomgenerator提供种子以及基于被提供的种子是在计算中使用的randomgenerators正在处理的数字的哈希码,结果是可重复的。无论线程的数量和实例化的顺序如何。

我想知道是否有可能为每个线程实例化一个随机生成器。下面的代码片段似乎可以实现这一点,但由于随机生成器不再提供(可再生)种子,结果不可重复。

class Program { 
    static void Main(string[] args) { 

     var seed = 10; 
     var data = new List<double>(); 
     var dataGenerator = new Random(seed); 

     for (int i = 0; i < 10000; i++) { 
      data.Add(dataGenerator.NextDouble()); 
     } 

     var results = new ConcurrentBag<double>(); 

     var localRandom = new ThreadLocal<Random>(() => new Random()); 

     Parallel.ForEach(data, (d) => { 
      var result = Calculate(d, localRandom.Value); 
      results.Add(result); 
     }); 

    } 

    static double Calculate(double x, Random random) { 
     return x * random.NextDouble(); 
    } 
} 

任何人都可以想出一个很好的解决这个问题?

+0

我不认为这是可能的。第一种解决方案的工作原理是因为你的随机数生成器与线程数无关。在第二种情况下,当您为每个线程创建一个随机生成器时,即使您找到一致的方式来初始化种子,结果也将取决于线程的数量。也许做一个Random的自定义实现,让你每次生成一个新的数字时都提供一个种子? – 2011-12-14 09:43:31

+0

无赖。我认为每次设置一个新的种子就像每次实例化一个新的随机一样。 – jkokorian 2011-12-14 11:08:21

回答

3

这是可能的,你的问题的确可以做得很准确,但问题在于那不是你想要的。

如果你每次用种类相同的号码为你的线程本地Random播种,那么你会在该线程中确定结果,这与前面的操作数有关。你想要的是一个伪随机数,相对于输入是确定的。

那么,你可以坚持Random()。这并不重要。

或者,您可以拥有自己的伪随机算法。下面是基于重新散列算法(旨在更好地分配散列码的位)一个简单的例子:

private static double Calculate(double x) 
{ 
    unchecked 
    { 
    uint h = (uint)x.GetHashCode(); 
    h += (h << 15)^0xffffcd7d; 
    h ^= (h >> 10); 
    h += (h << 3); 
    h ^= (h >> 6); 
    h += (h << 2) + (h << 14); 
    return (h^(h >> 16))/(double)uint.MaxValue * x; 
    } 
} 

这是不是一个特别好的伪随机生成的,但它是相当快的。它也没有分配,导致没有垃圾收集。

这里存在着这种整个方法的权衡;你可以简化上述步骤,甚至可以更快但更少的“随机”,或者你可以更“随机”的更多努力。我敢肯定,那里的代码比上面的代码更快,更“随机”,这更多的是为了展示这种方法,而不是其他任何方法,但是在你考虑的竞争对手算法中,生成的数量与性能。 new Random(d).NextDouble()在这种折衷中处于特殊的地位,其他方法在其他方面。

编辑:我使用的重新哈希算法是一个王/詹金斯哈希。当我写下这个名字时,我不记得这个名字。

编辑:具有从评论你的要求一个更好的主意,我现在说......

你想创建一个PRNG类,它可以使用上面的算法,即中System.Random(以反射代码作为起点),您提到的128bitXorShift算法或其他。重要的区别是它必须有一个Reseed方法。例如,如果你复制了System.Random的方法,你的种子将看起来像构造函数的大部分体(事实上,你可能会重构,所以除了可能创建它使用的数组之外,构造函数会调用重新设置)。

然后,您会为每个线程创建一个实例,并在您现有代码中创建新的Random时调用.Reseed(d.GetHashCode())

还要注意,这给了你另一个优点,即如果你依赖于你的PRNG的一致结果(看起来你是这么做的),那么你不承诺在框架版本之间的System.Random中有一致的算法甚至可能包括修补程序和安全修复程序)对您而言是一个坏点,并且此方法会增加一致性。

但是,你也没有答应一致的算法double.GetHashCode()。我怀疑他们会改变它(不像string.GetHashCode(),这是经常发生变化),但以防万一,你可以让你的Reseed()采取双重做这样的事情:

private static unsafe int GetSeedInteger(double d) 
{ 
    if(d == 0.0) 
    return 0; 
    long num = *((long*)&d); 
    return ((int)num)^(int)(num >> 32); 
} 

这非常简单,只是将当前double.GetHashCode() ,但现在你将面对框架变化时保持一致。

这可能是值得考虑打破了一组任务分成块自己,因为每个块创建线程,然后只需在每块方法创建该对象为本地。

优点:

访问ThreadLocal<T>比访问本地T更加昂贵。

如果任务是在相对时间执行一致的,你不需要很多Parallel.ForEach的聪明。

缺点:

Parallel.ForEach是在平衡东西出来真的很好。你在做的事情必须非常自然地平衡,或者在避开它的使用获得任何东西之前,在前块基础上节省很多。