仿真给出了不同的结果,正常的回路Vs并联

时间:2012-01-20 16:50:25

标签: c# parallel-processing task-parallel-library

当我尝试使用普通for循环(这是正确的结果)时,我对我的一个简单模拟样本的不同结果感到有点惊讶.Vs 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);
    }
}

}

先谢谢

2 个答案:

答案 0 :(得分:4)

少数事情:

  1. Random类不是线程安全的。每个工作线程需要一个新的Random实例。
  2. 您以非线程安全的方式递增matches变量。您可能希望使用Interlocked.Increment(ref matches)来保证线程在增加变量时的安全性。
  3. 你的for循环和你的Parallel :: For没有执行完全相同的次数,因为你在for循环中执行了&lt; =并且Parallel :: For的第二个参数是独占,所以在这种情况下,您需要为路径添加1以使它们等效。
  4. 试试这个:

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

答案 1 :(得分:2)

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

我看到的其他问题:
- 您的串行循环包括j等于trails的迭代,而并行循环则没有(末尾索引在Parallel.For中是独占的);
- class Random可能不是线程安全的。


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

  

因为时钟具有有限的分辨率,使用无参数构造函数以紧密连续的方式创建不同的随机对象会创建随机数生成器,从而生成相同的随机数序列。

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

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

但是,我注意到Random的初始化进入循环后,所有加速都丢失了(在我的Intel Core 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实例的哈希码初始化每线程随机函数。