在 Parallel.For 中使用随机

Using Random in Parallel.For

我的程序进行了多次模拟,每次模拟都需要生成许多随机数。串行方法简单明了并且有效。然而,在我追求并行化工作的过程中,我相信我创造了一种比我能找到的方法更直接的方法。其他方法有些过时,有些事情现在可能是不可能的。

我是否遗漏了一些东西,使我的方法容易受到无数多线程问题的影响?我的方法使用 Parallel.For 的能力为单个线程使用实例化变量,因此它不需要像我发现的其他方法那样的另一个 class。在这种情况下,每个线程都有自己的 Random.

时间:

我的方法:4s

斯蒂芬:14s

乔恩:16 秒

显然我不像斯蒂芬或乔恩那么了解所以我担心我错过了什么。

我的方法:

Random rnd = new Random();
int trials = 1_000_000;

private readonly object globalLock = new object();
ParallelOptions po = new ParallelOptions();
po.MaxDegreeOfParallelism = 4;

await Task.Run(() =>
{
    Parallel.For<Random>(0, trials, po, 
    () => { lock(globalLock){ return new Random(rnd.Next()); } }, 
    (i, loop, local) =>
    {
        for (int work = 0; work < 1000; work++)
        {
            local.Next();
        }

        return local;
    },
        (x) => { }
    );
});

下一个方法由 Stephen Toub 在 MSDN Blog:

public static class RandomGen2
{
    private static Random _global = new Random();
    [ThreadStatic]
    private static Random _local;

    public static int Next()
    {
        Random inst = _local;
        if (inst == null)
        {
            int seed;
            lock (_global) seed = _global.Next();
            _local = inst = new Random(seed);
        }
        return inst.Next();
     }
}

await Task.Run(() =>
{
    Parallel.For(0, trials, i =>
    {
        for (int work = 0; work < 1000; work++)
        {
            RandomGen2.Next();
        }
    });

});

下一个方法由 Jon Skeet 在 his blog:

public static class ThreadLocalRandom
{
    private static readonly Random globalRandom = new Random();
    private static readonly object globalLock = new object();

    private static readonly ThreadLocal<Random> threadRandom = new ThreadLocal<Random>(NewRandom);

    public static Random NewRandom()
    {
        lock (globalLock)
        {
            return new Random(globalRandom.Next());
        }
    }

    public static Random Instance { get { return threadRandom.Value; } }

    public static int Next()
    {
        return Instance.Next();
    }
}

await Task.Run(() =>
{
    Parallel.For(0, trials, i =>
    {
        for (int work = 0; work < 1000; work++)
        {
            ThreadLocalRandom.Instance.Next();
        }
    });
});

Update/answer: Brian 指出我错误地使用了 Jon 的方法。更正确的方法是为每个 Parallel.For 循环调用 ThreadLocalRandom.Instance 并将该实例用于内部 for 循环。这样可以防止每次调用都进行线程检查,而是每个 Parallel.For 循环只有一个线程检查。正确使用 Jon 的方法使他的方法比我使用的 Parallel.For 的重载更快。

However, in my pursuit of parallelizing the work I believe I found a more straightforward method than what I could find.

比较直接,但是错了。

The other methods are somewhat dated.

这到底是什么意思?

Am I missing something that will make my method susceptible to any of the myriad of multithreading problems?

线程安全最基本的规则是:不能在没有锁的情况下在多个线程上使用一个非线程安全的对象。 Random 不是线程安全的,但您在每个线程上使用相同的线程来计算种子。

请注意,Jon 和 Stephen 的 "dated" 方法正确地锁定了播种随机数。

Clearly I don't know as much as Stephen or Jon so I'm concerned I missed something.

首先,在编写更多多线程代码之前,您应该彻底内化线程安全的基本规则。

其次,你的态度是你的错误。正确的态度是:Jon 和 Stephen 都是专家,他们的解决方案没有不必要的部分。如果您认为您找到的解决方案缺少他们的解决方案所具有的部分,那么您需要解释为什么您的解决方案不需要他们的解决方案所具有的部分

您的代码速度更快,因为它更简单。您的代码为每个循环提供了 Random 的专用实例。 Jon 和 Stephen 的代码也是这样做的,但是在他们的代码中,每次访问 Random 都必须检查哪个线程正在使用,然后提取 Random 的正确实例。 Stephen 的代码比 Jon 的代码快,因为 ThreadLocal(它是 ThreadStatic 的包装器)稍慢。

然而,他们的代码的好处在于他们的代码提供了 Random 的简单替代品。您的方法将责任放在并行代码上以初始化 Random。在实际问题中,与拥有静态、线程安全的 Random 服务相比,跨各种支持函数携带 Random 的实例有点麻烦。

在实际任务中,您的功能可能不会被调用 Random 所支配。所以在正常情况下,他们的代码有轻微的性能损失是可以的。

我推荐 ThreadLocal<T> 而不是 ThreadStatic(讨论见 ThreadStatic v.s. ThreadLocal<T>: is generic better than attribute?)。

顺便说一句,请不要将 lock 用于除专用锁对象之外的任何对象。和 Jon (https://codeblog.jonskeet.uk/2008/12/05/redesigning-system-object-java-lang-object/) 一样,我真的希望 lock 甚至不支持任意对象。