模拟给出不同的结果与正常的环路Vs并行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(); 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(); 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); } } 

}

提前致谢

该代码包含有关matches更新的数据竞争 。 如果两个线程同时执行,则两者都可以读取它的相同值(例如,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 ThreadRnd = new ThreadLocal(() => { return new Random(Thread.CurrentThread.GetHashCode()); }); Parallel.For(0, trails + 1, j => { Random rnd = ThreadRnd.Value; /* ... */ }); 

注意如何使用当前运行的Thread实例的哈希码初始化每线程随机函数。

一些事情:

  1. Random类不是线程安全的。 每个工作线程需要一个新的Random实例。
  2. 您以非线程安全的方式递增matches变量。 您可能希望使用Interlocked.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(); 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); }