将单线程应用程序迁移到多线程,并行执行,蒙特卡罗模拟

时间:2009-07-12 18:24:55

标签: c# multithreading parallel-processing threadpool

我的任务是采用现有的单线程蒙特卡罗模拟优化。这是ac#console app,没有数据库访问它从csv文件加载数据一次并在最后写出来,所以它几乎只是CPU绑定,也只使用大约50mb的内存。 / p>

我通过Jetbrains dotTrace探查器运行它。在总执行时间中,大约30%生成均匀随机数,24%将均匀随机数转换为正态分布随机数。

基本的算法是一大堆嵌套for循环,在中心有随机数调用和矩阵乘法,每次迭代返回一个加到结果列表中的double,这个列表是定期的对某些收敛标准进行排序和测试(在检查点,每次迭代总计数的5%),如果可接受的话,程序会从循环中跳出并写入结果,否则它会一直进行到最后。

我希望开发者能够权衡:

  • 我应该使用新的Thread v ThreadPool
  • 我应该查看 Microsoft Parallels Extension库
  • 我应该查看 AForge.Net Parallel.For http://code.google.com/p/aforge/其他任何库吗?

上面的一些指向教程的链接非常受欢迎,因为我从未编写任何并行或多线程代码

  • 生成大量正态分布随机数的最佳策略,然后使用它们。应用程序从未在此状态下使用统一随机数,它们始终转换为正态分布然后消耗。
  • 用于随机数生成的良好快速库(并行?)
  • 内存注意事项,因为我采取此并行,我需要多少额外费用。

当前应用程序需要2个小时进行500,000次迭代,业务需要将其扩展到3,000,000次迭代,并且每天被称为多次,因此需要进行大量优化。

特别希望听到使用 Microsoft Parallels Extension AForge.Net Parallel 的人

这需要相当快地生产,所以 .net 4 beta已经,即使我知道它已经融入了并发库,我们可以看一下它之后的轨道迁移到.net 4释放。目前服务器有.Net 2,我已提交审核升级到.net 3.5 SP1,这是我的开发箱。

由于

更新

我刚尝试了Parallel.For实现,但它提出了一些奇怪的结果。 单线程:

IRandomGenerator rnd = new MersenneTwister();
IDistribution dist = new DiscreteNormalDistribution(discreteNormalDistributionSize);
List<double> results = new List<double>();

for (int i = 0; i < CHECKPOINTS; i++)
{
 results.AddRange(Oblist.Simulate(rnd, dist, n));
}

要:

Parallel.For(0, CHECKPOINTS, i =>
        {
           results.AddRange(Oblist.Simulate(rnd, dist, n));
        });

在模拟内部有很多调用rnd.nextUniform(),我认为我得到了许多相同的值,这是否可能发生,因为现在它是并行的?

也可能是List AddRange调用不是线程安全的问题?我看到了这个

System.Threading.Collections.BlockingCollection可能值得使用,但它只有Add方法没有AddRange所以我必须查看结果并以线程安全的方式添加。来自使用Parallel的人的任何见解。非常感谢。我暂时切换到 System.Random 我的调用因为我在使用Mersenne Twister实现调用nextUniform时遇到异常,可能它不是线程安全的某个数组得到索引越界 ....

3 个答案:

答案 0 :(得分:13)

首先,您需要了解为什么您认为使用多个线程是一种优化 - 实际上并非如此。如果您有多个处理器,使用多个线程将使您的工作负载完全,然后最多可以提供更快的CPU(这称为加速)。传统意义上的工作并没有“优化”(即工作量没有减少 - 实际上,多线程工作总量因为线程开销而增加。)

因此,在设计应用程序时,您必须找到可以以并行或重叠方式完成的工作。有可能并行生成随机数(通过在不同的CPU上运行多个RNG),但这也会改变结果,因为您获得了不同的随机数。另一个选择是在一个CPU上生成随机数,在不同CPU上生成其他所有内容。这可以使您的最大加速比为3,因为RNG仍将按顺序运行,并且仍然需要30%的负载。

因此,如果你进行这种并行化,你最终得到3个线程:线程1运行RNG,线程2运行正态分布,线程3完成其余的模拟。

对于这种架构,producer-consumer architecture是最合适的。每个线程将从队列中读取其输入,并将其输出生成到另一个队列中。每个队列都应该是阻塞的,因此如果RNG线程落后,则规范化线程将自动阻塞,直到新的随机数可用。为了提高效率,我会在线程中传递100(或更大)数组中的随机数,以避免在每个随机数上进行同步。

对于此方法,您不需要任何高级线程。只需使用常规线程类,没有池,没有库。您唯一需要的是(遗憾的是)不在标准库中的是阻塞Queue类(System.Collections中的Queue类并不好)。 Codeproject提供了一个看起来合理的实现;可能还有其他人。

答案 1 :(得分:1)

List<double>绝对不是线程安全的。请参阅System.Collections.Generic.List documentation中的“线程安全”部分。原因是性能:添加线程安全性不是免费的。

您的随机数实现也不是线程安全的;多次获得相同的数字正是您在这种情况下所期望的。让我们使用以下rnd.NextUniform()的简化模型来理解发生的事情:

  1. 从中计算伪随机数 对象的当前状态
  2. 更新对象的状态 下一次通话会产生不同的数字
  3. 返回伪随机数
  4. 现在,如果两个线程并行执行此方法,可能会发生以下情况:

    • 线程A计算随机数 如步骤1。
    • 线程B计算随机数 如步骤1中所示。线程A尚未进行 更新了对象的状态,所以 结果是一样的。
    • 线程A更新状态 对象,如步骤2。
    • 线程B更新了状态 对象在步骤2中,践踏A的状态 改变或者给予相同的改变 结果

    正如您所看到的,您可以做的任何证明rnd.NextUniform()有效的推理都不再有效,因为两个线程互相干扰。更糟糕的是,这样的错误取决于时间,并且在某些工作负载或某些系统下可能很少出现“故障”。调试噩梦!

    一种可能的解决方案是消除状态共享:为每个任务提供使用另一个种子初始化的随机数生成器(假设实例不以某种方式通过静态字段共享状态)。

    另一个(劣等)解决方案是在MersenneTwister类中创建一个包含锁定对象的字段,如下所示:

    private object lockObject = new object();
    

    然后在MersenneTwister.NextUniform()实施中使用此锁:

    public double NextUniform()
    {
       lock(lockObject)
       {
          // original code here
       }
    }
    

    这将阻止两个线程并行执行NextUniform()方法。您Parallel.For中列表的问题可以通过类似方式解决:将Simulate电话和AddRange电话分开,然后在AddRange电话周围添加锁定。

    我的建议:尽可能避免在并行任务之间共享任何可变状态(如RNG状态)。如果没有共享可变状态,则不会发生线程问题。这也避免了锁定瓶颈:您不希望“并行”任务等待一个根本不并行工作的随机数生成器。特别是如果有30%的时间用于获取随机数字。

    将状态共享和锁定限制在无法避免的地方,例如在汇总并行执行结果时(如AddRange次调用)。

答案 2 :(得分:0)

线程将变得复杂。您必须将程序分解为逻辑单元,每个单元都可以在自己的线程上运行,并且您将不得不处理出现的任何并发问题。

并行扩展库应该允许您通过将一些for循环更改为 Parallel.For 循环来并行化您的程序。如果你想看看它是如何工作的,Anders Hejlsberg和Joe Duffy在这里的30分钟视频中提供了一个很好的介绍:

http://channel9.msdn.com/shows/Going+Deep/Programming-in-the-Age-of-Concurrency-Anders-Hejlsberg-and-Joe-Duffy-Concurrent-Programming-with/

线程与ThreadPool

正如其名称所示,ThreadPool是一个线程池。使用ThreadPool获取线程有一些优点。线程池使您可以通过为应用程序提供由系统管理的工作线程池来更有效地使用线程。