【问题标题】:Parallel.For and For yield different resultsParallel.For 和 For 产生不同的结果
【发布时间】:2012-11-13 12:04:20
【问题描述】:

如果我运行这个测试:

 var r = new Random();
 var ints = new int[13];
 Parallel.For(0, 2000000, i => {            
     var result = r.Next(1, 7) + r.Next(1, 7);
     ints[result] += 1;
 });

我得到以下结果:

2: 92,14445
3: 0,41765
4: 0,62245
5: 0,82525
6: 1,04035
7: 1,25215
8: 1,0531
9: 0,8341
10: 0,6334
11: 0,4192
12: 0,2109

当我使用常规 For 时:

for (int i = 0; i < 2000000; i++) {
    var result = r.Next(1, 7) + r.Next(1, 7);
    ints[result] += 1;
}

输出是:

2: 2,7797
3: 5,58645
4: 8,3414
5: 11,09935
6: 13,8909
7: 16,6731
8: 13,82895
9: 11,10205
10: 8,3424
11: 5,5712
12: 2,7845

最后一个结果是三角分布,它是预期的输出。

我的问题的目的不是讨论并行性的适用性。问题是为什么 Parallel.For 会这样?

【问题讨论】:

  • 你能显示运行这两个测试的 100 个结果吗?
  • 你的机器有多少个处理器?
  • 当你掷两个骰子时,你通常会得到2,除非你是连续的;-)
  • 您确定您拥有的是正态分布吗?在我看来,它看起来更像是一个以 7 为中心的三角形。

标签: c# multithreading c#-4.0 parallel-processing


【解决方案1】:

Random 类方法不是线程安全的。

http://msdn.microsoft.com/en-us/library/system.random.next(v=vs.90).aspx#2

所以第一段代码只是演示了一些未定义的行为。

编辑:

至于一些猜测,根据我对操作系统的了解,我相信随机数生成是一个相当低级的操作,因此甚至可能需要上下文切换。在发生这种情况时,您最终可能会在有机会更新之前多次获取相同的随机数。这将解释不平衡的分布。

【讨论】:

  • 查看here 了解使Random 线程安全的方法。
【解决方案2】:

除了@spencerruport 断言 Random 类不是线程安全的之外,您的并行代码也不是线程安全的:

 Parallel.For(0, 2000000, i => {            
     //say two threads produce same total at same time
     var result = r.Next(1, 7) + r.Next(1, 7); 
     //what happens on the next line when a context-switch
     //occurs during this non-atomic operation?
     ints[result] += 1;
 });

利用 PLINQ 代表您收集结果可能会更好:

Enumerable.Range(0, 2000000)
    .AsParallel()
    .Select(_ => SafeRandom(1, 7) + SafeRandom(1, 7))
    .GroupBy(x => x)
    .Select(g => new {value = g.Key, frequency = g.Count()})

而不是自己管理对共享内存(您上面的ints 数组)的访问。

SafeRandom 的合理实现可能如下所示:

private static int seedUnique=0;
private static ThreadLocal<Random> tlRand=new ThreadLocal<Random>(() => {
    var x=Interlocked.Add(ref seedUnique, 93459872);
    var r=new Random((int)(DateTime.UtcNow.Ticks + x));
    return r;
});
public static int SafeRandom(int min, int max)
{
    return tlRand.Value.Next(min,max);
}

【讨论】:

【解决方案3】:

这是Random的线程安全。

一旦我调用了Random.Next() 线程安全,我就会按预期得到以下分布。

2: 2.76665
3: 5.5382
4: 8.30805
5: 11.13095
6: 13.8864
7: 16.6808
8: 13.8722
9: 11.14495
10: 8.3409
11: 5.5631
12: 2.76775

public static class Program
{
    private const int Max = 2000000;
    private static readonly object Lock = new object();

    public static void Main()
    {
        var r = new Random();
        var ints = new int[13];
        Parallel.For(0, Max, i =>
        {
            var result = Rand(r, 1, 7) + Rand(r, 1, 7);
            Interlocked.Increment(ref ints[result]);
        });

        for (int i = 0; i < ints.Length; i++)
        {
            Console.WriteLine("{0}: {1}",
                i, ints[i] / ((double)Max) * 100);
        }
    }

    private static int Rand(Random random, int minValue, int maxValue)
    {
        lock (Lock)
        {
            return random.Next(minValue, maxValue);
        }
    }
}

【讨论】:

  • 是的,但在Parallel.For 的主体中仍然不是线程安全的。
  • 并行访问共享的ints 数组。您将有许多线程在数组的相同索引上运行。看我的回答。你的计数加起来是 2000000 吗?我敢打赌不会,因为这个问题(虽然问题可能通过传递上一个方法调用上的锁来缓解,但我不会依赖这个)。
  • @spender 在我相信 ++ 不是线程安全的说法之前,我想在 JIT 运行后或微软的权威来源看到一些 x64 代码。除非 Microsoft 竭尽全力使其不是线程安全的,否则 ++ 是单个原子操作。
  • 加载,是的。补充,是的。商店,是的。加载、添加和存储?对不起,你错了。为什么你认为互锁类存在? msdn.microsoft.com/en-us/library/dd78zt0c.aspx
  • 看起来 Eric Lippert 在此之前已解决过此问题,stackoverflow.com/questions/4628243/is-the-operator-thread-safe。很难理解 JIT 团队是如何决定采用一条 CPU 指令并将其变成多条的。
猜你喜欢
  • 1970-01-01
  • 2013-08-12
  • 1970-01-01
  • 2019-06-03
  • 1970-01-01
  • 2018-05-23
  • 2017-09-11
  • 2020-03-14
  • 2015-03-11
相关资源
最近更新 更多