【问题标题】:Simulation gives different result with normal for loop Vs Parallel For模拟给出了不同的结果,正常的 for 循环 Vs Parallel For
【发布时间】:2012-01-22 02:02:01
【问题描述】:

当我尝试使用正常的 for 循环(这是正确的结果)与 Parallel 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# parallel-processing task-parallel-library


    【解决方案1】:

    几件事:

    1. Random 类不是线程安全的。每个工作线程都需要一个新的 Random 实例。
    2. 您正在以非线程安全的方式递增matches 变量。您可能希望使用Interlocked.Increment(ref matches) 来保证增加变量时的线程安全。
    3. 您的 for 循环和 Parallel::For 执行的次数不完全相同,因为您在 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);
    }
    

    【讨论】:

    • 嗨 Drew,您的所有观点都是有效的,非常感谢。我复制了新代码,但我仍然想知道并行方法没有给出与普通方法相同的结果。我怀疑我的 Parallel 方法中是否还有更多错误。如果我们创建新实例,我不确定每次随机是如何工作的,它会起作用,但我有点怀疑它是否像我的正常方法一样工作。现在并行方法的结果是 0.548,正常是 0.507。我需要达到 .507,这是准确的...您能多想一点吗..谢谢您的评论
    • 你是说每次的结果都应该完全一样?如果我多次执行您的“正常”实现,我每次都会得到不同的结果。它们总是在一个范围内,但是......仍然不同。就匹配“正常”实现的值而言,您最初的并行实现总是相距甚远,但新的并行实现更接近。由于我不太了解算法的值是什么,因此我不确定如何准确判断我在寻找什么。
    • 既然是统计算法,结果应该不会和串口码完全一样。然而,由于Random 的所有实例默认初始化,算法中缺乏随机化可能会产生显着差异。我在回答中提供了更多细节。
    【解决方案2】:

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

    我看到的其他问题:
    - 您的串行循环包含一个迭代 j 等于 trails 而并行循环不包含(结束索引在 Parallel.For 中是唯一的);
    - class Random 可能不是线程安全的。


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

    因为时钟具有有限的分辨率,所以使用无参数构造函数来创建不同的 Random 对象,从而创建产生相同随机数序列的随机数生成器。

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

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

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


    更新 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 实例的哈希码初始化的。

    【讨论】:

    • 嗨,阿列克谢。谢谢你的cmets。我相信你的观点是有效的..但不知何故,与串行相比,我的并行匹配数量更多。你能指出任何其他问题吗?...在​​此先感谢。我已经纠正了两种方法中的迭代不匹配
    • 嗨,阿列克谢。你太棒了......你的解决方案完美无缺。太感谢了。以后和你聊天。
    猜你喜欢
    • 1970-01-01
    • 2015-06-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-01-16
    • 2017-03-30
    • 2016-04-12
    • 1970-01-01
    相关资源
    最近更新 更多