提问人:Justin Mathew 提问时间:1/21/2012 最后编辑:Justin Mathew 更新时间:1/22/2012 访问量:1056
仿真给出了不同的结果,包括正常 for 循环与并行 for
Simulation gives different result with normal for loop Vs Parallel For
问:
当我尝试使用正常的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);
}
}
}
提前致谢
答:
该代码包含 更新时的数据争用。如果两个线程同时执行此操作,则两个线程都可以读取相同的值(例如,10),然后都将其递增(到11)并写回新值。因此,注册的匹配项会更少(在我的示例中,是 11 个而不是 12 个)。解决方案是将 System.Threading.Interlocked
用于此变量。matches
我看到的其他问题:
- 您的串行循环包括一个迭代 for equal to,而并行循环没有(结束索引在 Parallel.For
中是排他性的);
- 类 Random
可能不是线程安全的。j
trails
更新:我认为 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# 专家,我不知道为什么;但我认为该类可能具有所有实例共享的一些数据,并具有访问同步功能。Random
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
评论
几件事:
- Random 类不是线程安全的。每个工作线程都需要一个新的 Random 实例。
- 您正在以非线程安全的方式递增变量。您可能希望使用它来保证递增变量的线程安全性。
matches
Interlocked.Increment(ref matches)
- 您的 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);
}
评论
Random
评论