0

我正在努力寻找一种方法让每个线程都有一个随机数生成器,同时确保在重新运行程序时产生相同的数字。

我现在做的是这样的:

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();
    }
}

因为为创建“数据”列表的随机生成器提供了一个种子,并且为计算中使用的随机生成器提供了一个基于正在处理的数字的哈希码的种子,所以结果是可重复的。不管线程的数量和它们被实例化的顺序。

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

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();
    }
}

谁能想到一个很好的解决这个问题的方法?

4

1 回答 1

4

有可能,确实你在你的问题中几乎正确地做到了,但问题是这并不是你想要的。

如果您每次都使用相同的数字为您的线程本地播种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()在该权衡中处于特定点,其他方法在其他点。

编辑:我使用的重新散列算法是 Wang/Jenkins 散列。写的时候想不起名字了。

编辑:从评论中更好地了解您的要求,我现在想说...

您想创建一个 PRNG 类,它可以使用上面的算法、System.Random(以反射代码为起点)、您提到的 128bitXorShift 算法或其他算法。重要的区别是它必须有一个Reseed方法。例如,如果你复制了System.Random's 方法,你的 reseed 看起来就像构造函数的大部分主体(实际上,你可能会重构,以便除了创建它使用的数组之外,构造函数还会调用 reseed)。

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

另请注意,这为您提供了另一个优势,即如果您依赖于 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);
}

这几乎只是复制了 current double.GetHashCode(),但现在你将在面对框架更改时保持一致。

可能值得考虑自己将一组任务分成块,为每个块创建线程,然后在 per-chunk 方法中将此对象创建为本地对象。

优点:

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

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

缺点:

Parallel.ForEach真的很擅长平衡事情。在避免使用它之前,您所做的事情必须非常自然地平衡,或者在前大块的基础上节省大量资金。

于 2011-12-14T11:30:22.760 回答