使用已知种子创建ThreadLocal随机生成器

时间:2011-12-14 09:21:46

标签: c# random parallel-processing thread-local-storage

我很难找到一种方法,每个线程都有一个随机数生成器,同时确保在重新运行程序时生成相同的数字。

我现在做的是这样的事情:

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

有人能想出解决这个问题的好方法吗?

1 个答案:

答案 0 :(得分: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()处于权衡的特定点,其他方法则处于其他方面。

编辑:我使用的重新哈希算法是Wang / Jenkins哈希。我写这篇文章的时候记不住这个名字了。

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

你想创建一个PRNG类,它可以使用上面的算法,System.Random(以反射代码为起点),你提到的128bitXorShift算法或其他什么。重要的区别是它必须有Reseed方法。例如,如果你复制System.Random的方法,你的种子将看起来像构造函数的大部分(实际上,你可能会重构,所以除了可能创建它使用的数组之外,构造函数会调用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);
}

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

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

优点:

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

如果任务在相对执行时间上保持一致,则不需要很多Parallel.ForEach的聪明才智。

缺点:

Parallel.ForEach非常善于平衡事情。你正在做的事情必须非常自然地平衡,或者在避免使用任何东西之前在前块上节省很多。