仿真给出了不同的结果,包括正常 for 循环与并行 for

Simulation gives different result with normal for loop Vs Parallel For

提问人:Justin Mathew 提问时间:1/21/2012 最后编辑:Justin Mathew 更新时间:1/22/2012 访问量:1056

问:

当我尝试使用正常的for循环(这是正确的结果)与并行For时,我对我的一个简单模拟样本的不同结果感到有点惊讶。请帮我找出可能的原因。我观察到,与正常情况相比,并行执行速度非常快。

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Simulation
{
    class Program
    {

    static void Main(string[] args)
    {
       ParalelSimulation(); // result is .757056
       NormalSimulation();  // result is .508021 which is correct
        Console.ReadLine();
    }

    static void ParalelSimulation()
    {
        DateTime startTime = DateTime.Now;

        int trails = 1000000;
        int numberofpeople = 23;
        Random rnd = new Random();
        int matches = 0;

        Parallel.For(0, trails, i =>
            {
                var taken = new List<int>();
                for (int k = 0; k < numberofpeople; k++)
                {
                   var day = rnd.Next(1, 365);
                    if (taken.Contains(day))
                    {
                        matches += 1;
                        break;
                    }
                    taken.Add(day);
                }
            }
        );
        Console.WriteLine((Convert.ToDouble(matches) / trails).ToString());
        TimeSpan ts = DateTime.Now.Subtract(startTime);
        Console.WriteLine("Paralel Time Elapsed: {0} Seconds:MilliSeconds", ts.Seconds + ":" + ts.Milliseconds);
    }
    static void NormalSimulation()
    {
        DateTime startTime = DateTime.Now;

        int trails = 1000000;
        int numberofpeople = 23;
        Random rnd = new Random();
        int matches = 0;

        for (int j = 0; j < trails; j++)
        {
            var taken = new List<int>();
            for (int i = 0; i < numberofpeople; i++)
            {
               var day = rnd.Next(1, 365);
                if (taken.Contains(day))
                {
                    matches += 1;
                    break;
                }
                taken.Add(day);
            }
        }
        Console.WriteLine((Convert.ToDouble(matches) / trails).ToString());
        TimeSpan ts = DateTime.Now.Subtract(startTime);
        Console.WriteLine(" Time Elapsed: {0} Seconds:MilliSeconds", ts.Seconds + ":" + ts.Milliseconds);
    }
}

}

提前致谢

C# 并行处理 任务并行库

评论


答:

2赞 Alexey Kukanov 1/21/2012 #1

该代码包含 更新时的数据争用。如果两个线程同时执行此操作,则两个线程都可以读取相同的值(例如,10),然后都将其递增(到11)并写回新值。因此,注册的匹配项会更少(在我的示例中,是 11 个而不是 12 个)。解决方案是将 System.Threading.Interlocked 用于此变量。matches

我看到的其他问题:
- 您的串行循环包括一个迭代 for equal to,而并行循环没有(结束索引在 Parallel.For 中是排他性的);
- 类 Random 可能不是线程安全的。
jtrails


更新:我认为 Drew Marsh 的代码没有得到你想要的结果,因为它没有提供足够的随机化。每个 1M 实验都以完全相同的随机数开始,因为您使用默认种子启动了 Random 的所有本地实例。从本质上讲,你重复同一个实验 1M 次,所以结果仍然是有偏差的。要解决此问题,您需要每次为每个随机化器添加一个新值。更新:我在这里并不完全正确,因为默认初始化使用系统时钟作为种子;但是,MSDN 警告说

由于时钟的分辨率有限,因此使用无参数构造函数连续创建不同的 Random 对象会创建随机数生成器,这些生成器会生成相同的随机数序列。

因此,这仍然可能是随机化不足的原因,使用显式种子可能会获得更好的结果。例如,使用外循环迭代的次数进行初始化为我提供了一个很好的答案:

Parallel.For(0, trails + 1, j =>
{
    Random rnd = new Random(j); // initialized with different seed each time
    /* ... */          
});

但是,我注意到,在初始化进入循环后,所有加速都丢失了(在我的英特尔酷睿 i5 笔记本电脑上)。由于我不是 C# 专家,我不知道为什么;但我认为该类可能具有所有实例共享的一些数据,并具有访问同步功能。RandomRandom


更新 2:通过使用 ThreadLocal 为每个线程保留一个实例,我获得了良好的准确性和合理的加速:Random

ThreadLocal<Random> ThreadRnd = new ThreadLocal<Random>(() =>
{
    return new Random(Thread.CurrentThread.GetHashCode());
});
Parallel.For(0, trails + 1, j =>
{
    Random rnd = ThreadRnd.Value;
    /* ... */          
});

请注意,如何使用当前正在运行的 实例的哈希代码初始化每线程随机化程序。Thread

评论

0赞 Justin Mathew 1/21/2012
嗨,阿列克谢。感谢您的评论。我相信你的观点是有道理的。但不知何故,与串行相比,我在并行中获得的匹配次数更多。你能指出其他问题吗?...提前致谢。我已经纠正了两种方法中的迭代不匹配
0赞 Justin Mathew 1/23/2012
嗨,阿列克谢。你很优秀......你的解决方案工作得很好。非常感谢。以后和你聊天。
4赞 Drew Marsh 1/21/2012 #2

几件事:

  1. Random 类不是线程安全的。每个工作线程都需要一个新的 Random 实例。
  2. 您正在以非线程安全的方式递增变量。您可能希望使用它来保证递增变量的线程安全性。matchesInterlocked.Increment(ref matches)
  3. 您的 for 循环和 Parallel::For 执行的次数并不完全相同,因为您在 for 循环中执行了 <=,而 Parallel::For 的第二个参数是独占的,因此在这种情况下,您需要将 1 添加到跟踪中以使它们等效。

试试这个:

static void ParalelSimulationNEW()
{
    DateTime startTime = DateTime.Now;

    int trails = 1000000;
    int numberofpeople = 23;
    int matches = 0;

    Parallel.For(0, trails + 1, _ =>
    {
        Random rnd = new Random();

        var taken = new List<int>();
        for(int k = 0; k < numberofpeople; k++)
        {
            var day = rnd.Next(1, 365);
            if(taken.Contains(day))
            {
                Interlocked.Increment(ref matches);
                break;
            }
            taken.Add(day);
        }
    });
    Console.WriteLine((Convert.ToDouble(matches) / trails).ToString());
    TimeSpan ts = DateTime.Now.Subtract(startTime);
    Console.WriteLine("Paralel Time Elapsed: {0} Seconds:MilliSeconds", ts.Seconds + ":" + ts.Milliseconds);
}

评论

0赞 Justin Mathew 1/21/2012
嗨,德鲁,您的所有观点都是有效的,非常感谢。我复制了新代码,但我仍然想知道并行方法没有给出与普通方法相同的结果。我怀疑我的 Parallel 方法中几乎没有更多的错误。我不确定如果我们创建新实例,每次随机如何工作,它都会起作用,但我有点怀疑它是否像我的正常方法一样工作。现在并行方法的结果是 .548,正常值是 .507。我需要达到 .507,这是准确的......你能不能再想一想......感谢您的评论
0赞 Drew Marsh 1/21/2012
你是说每次的结果都应该完全一样吗?如果我多次执行您的“正常”实现,我每次都会得到不同的结果。它们总是在一个范围内,但是......不同,不少。在匹配“正常”实现的值方面,原始并行实现总是相差甚远,但新的并行实现要接近得多。由于我不太了解算法的值是什么,因此我不确定如何准确判断我在寻找什么。
0赞 Alexey Kukanov 1/22/2012
由于它是一种统计算法,因此结果不应与序列代码中的结果完全相同。但是,由于默认情况下初始化的所有实例,算法中缺少随机化,因此可能会产生显着差异。我在回答中放了更多细节。Random