在对任务进行并行化时,为了获得最佳性能,最重要的目标之一是要最小化这些临界区。大部分情况下,都不可能在两个并行部分之间避免运行串行代码,因为总需要启动并行作业并收集结果。然而,对临界区的代码进行优化,并且移除不必要的临界区甚至比并行代码的正确调优还要重要。
当您面对一个具有太多临界区的执行计划的时候,请不要忘记Amdahl法则。如果不能减少这些临界区,那么可以尝试找到一些可以与临界区并行运行的任务。例如,可以在临界区运行的时候并行运行一个任务预取数据供下一段并行算法使用,这样可以增强这种方案的整体性能。重要的是要考虑现代多核硬件能够提供的能力,而不是仅仅考虑只有一个单独的执行单元的情况。
1.6.5 理解多核并行程序的设计原则
James Reinders在Dr. Dobb’s Journal发表了一篇名为“Rules for Parallel Programming for Multicore”的文章(www.drdobbs.com/hpc-high-performance-computing/201804248)。他列举出了8条原则帮助多核程序设计的开发人员。他的原则也适合于创建并行的C#和.NET Framework 4应用程序。这8条原则是:
(1) 按照并行的方式思考——这一条原则指的是以并行的思想指导设计,前面的小节解释了这个内容。
(2) 使用抽象编程——您可以充分利用.NET Framework 4中的Task Parallel Library (TPL)所提供的新功能,使您的高层次代码反映问题本身,而不是复杂的底层线程管理技术。第2章介绍了TPL。
(3) 按照任务(事情)编程,而不是按照线程(CPU内核)编程——通过TPL进行程序设计,您可以编写代码实现基于任务的设计,而不用关注底层的线程。
(4) 设计的时候要考虑关闭并发的情形——通过TPL编写的代码在单核微处理器的计算机(只有一个物理内核的计算机)上也能够运行。
(5) 避免使用锁——非常重要的一点在于:要充分利用新的类、方法和数据结构,这些结构在设计上避免了复杂同步机制的必要性。TPL在很多复杂的情况下使得避免使用重量级的锁更加简单,TPL还提供了新的轻量级的同步机制。
(6) 利用为帮助并发而设计的工具和库——Visual Studio 2010提供了新的工具用于调试、测试和调优并行代码。在本书中,您将学习到很多这类工具和库。
(7) 使用可扩展的内存分配器——TPL在公共语言运行时(Common Language Runtime,CLR)中提供了可扩展的内存分配器,而且在使用任务和线程的时候会自动使用这些内存分配器。然而,为了最大限度地利用高速缓存,您必须分析各种不同的分区情形,尽量避免在每一个任务中耗费大量的内存。
(8) 设计的时候要考虑随增长的工作负载而扩展——掌握了并行扩展之后,就可以很容易地通过TPL提供的新类考虑Gustafson法则了。如果您的设计已经准备好了面对未来的扩展性,那么您编写的代码就可以随着内核的增长而扩展了。Windows 7和Windows Server 2008 R2支持最多256个硬件线程或逻辑处理器,因此,扩展空间还是很大的。