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

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

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

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

我希望开发人员能够权衡:

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

由于我从未编写任何并行或multithreading代码,因此欢迎使用上述教程的一些链接

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

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

特别想听听使用Microsoft Parallels ExtensionAForge.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 results = new List(); 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时遇到exception, 也许它不是线程安全的某个数组正在使索引超出界限 ….

首先,您需要了解为什么您认为使用多个线程是一种优化 – 实际上并非如此。 只有拥有多个处理器时,使用多个线程才能使您的工作负载更快完成,然后最多只有您可用CPU的速度 (这称为加速 )。 传统意义上的工作没有“优化”(即工作量没有减少 – 事实上,对于multithreading,由于线程开销,工作总量通常会增加)。

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

因此,如果您进行此并行化,最终会得到3个线程:线程1运行RNG,线程2运行正态分布,线程3执行其余模拟。

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

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

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

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

  1. 从对象的当前状态计算伪随机数
  2. 更新对象的状态,以便下一个调用产生不同的数字
  3. 返回伪随机数

现在,如果两个线程并行执行此方法,可能会发生以下情况:

  • 线程A计算随机数,如步骤1中所示。
  • 线程B计算随机数,如步骤1所示。线程A尚未更新对象的状态,因此结果相同。
  • 线程A更新对象的状态,如步骤2所示。
  • 线程B在步骤2中更新对象的状态,践踏A的状态更改或者可能给出相同的结果。

正如您所看到的,您可以做任何certificaternd.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调用中)。

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

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

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

线程与ThreadPool

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