性能,永远是程序员要考虑的问题。在单核时代,甚至在双核(多核)时代,一般是通过改善客户使用的计算机性能来提升程序的性能,如增加服务器、内存,配置负载均衡等手段来实现,我们称这个过程为享受性能免费大餐。天下没有免费的午餐,性能免费大餐也不能毫无止境,实际上,已经有了新的解决方案并行计算。并行计算就像是一道饕餮大餐而被人津津乐道,在本文中我们以烹饪为类比,通过对性能免费大餐的分析,使用 Visual Studio C++ 2010这把利器,应用并行编程模型大块朵颐的进行并行开发。
性能免费大餐已经结束
通过提升CPU的计算能力,确实能够改善应用程序性能。但在实际情况中,无论处理器性能提升多少,软件都有办法迅速吞噬。况且,计算机硬件毕竟受物理极限的约束,处理器主频的提升已经遇到了瓶颈。所以,享受性能免费大餐的日子已经结束,业界已经不能提供指数级增长的更快的处理器,而只能选择提供指数级增长的更多的处理器。多核将引领软件研发发生基础性的变化。
目前的电脑市场上,多核计算机的销量远远大于单核计算机,多核已经成为了一种主流。在这样的发展趋势下,如果把在单核下实现的应用程序拿到一台64核的机器上运行,你会看到任务管理器显示如下的画面只有1/64的计算能力得到了利用:
图 1 任务管理器显示64核机器上运行单线程应用程序,只有一个核在计算
通过上面的示例可以看出,传统的应用程序再也无法顺其自然地在更高端的硬件设备上获得更高的性能回报,能够充分发挥硬件设备性能的应用程序是未来软件开发的主流,作为开发人员,我们面临的抉择是什么呢?Herb Sutter在他的原文中明确地给出了答案:并行计算。如果在四年前说“并发将是软件开发史上的又一个重大变革”是一个预测,那么今天,并行计算已经成为软件开发的核心趋势之一。对于程序员来说,享受免费大餐的日子结束后,只能亲自下厨烹饪。
亲自下厨遇到了难题
我们都知道,应用程序的开发,有着完整的生命周期管理,从编写需求说明书、程序设计说明书,到编码、调试和性能优化,再到测试、发布,以及后期维护等一系列的行为都有其复杂性。而并行计算是在程序开发原有的复杂性上,更添加了一个维度。在这个过程中,程序员会遇到各种各样的问题,如下图所示:
图 2 开发并发应用程序面临的需要考虑的问题
开发并行应用程序,一直是令广大程序员头痛的事情,现如今我们又有哪些准备了呢。俗话说的好,巧媳妇难为无米之炊,一些准备好的食材、一本可供参考的菜谱、一套方便使用的厨具是必不可少的,希望这些必备品能为我们的烹饪带来便利。
食材、菜谱和厨具都很重要
2007年,微软宣布成立并行计算平台组,致力于简化并行应用程序的开发。随着Visual Studio 2010和.NET Framework 4的发布,微软为并行计算提供了完整的解决方案。
图 3 Visual Studio 2010和.NET Framework 4对并行开发的支持
食材并发运行时
不论是任务并行、数据并行,还是管道并行,都意味着需要把一个耗时的任务或数据分割成更小的单位。分割的颗粒度往往是由任务的性质决定的。细颗粒度并行的好处主要体现在可扩展性和负载平衡上。假设一个耗时的任务只被粗略分割成四个子任务并发执行,那么它对于多于四核的机器的扩展性就不够好;即使是在四核的机器上运行,也无法做到实时动态的负载平衡,可能发生三个子任务早早完成,而另一个任务还在一个核上苦苦等待的悲剧性事件。
为了支持细粒度并行,在Visual C++ 2010中提供了一套并发编程框架,支持常用的协同任务调度和硬件资源(CPU和内存)管理。.NET Framework 4也在已有的线程池(Thread Pool)基础上,实现了协同任务调度和工作窃取(work-stealing)算法。工作窃取算法充分利用了细颗粒度并行的优势,保证空闲的线程依照一定的顺序,从本地、全局,甚至是其他线程的任务队列中“偷取”任务执行。当然,默认的任务调度器可以被扩展或配置以支持特殊的调度策略。
并发运行时是 C++ 并发编程框架,使用该框架,可以简化并行编程,能够帮助我们编写可靠、可伸缩且具有响应能力的并行应用程序。
菜谱编程模型
高抽象级别的、统一的编程模式是简化并行程序开发的一个重要方向。因此在Visual Studio 2010和.NET Framework 4中看到许多新的语言和库功能,以及一系列面向任务的并行结构和算法。
在Visual C++ 2010中,并行模式库(Parallel Pattern Library)引入了支持任务并行的任务(Tasks)和任务组(Task Groups)概念。任务是一类计算,可将其分解为多个更精细的计算。在并行模式库中,task_handle包含执行细颗粒度的代码,用来代表一个任务。任务句柄(task_handle)非常重要,因为它们将管理封装的工作函数的生存期。 例如,将 task_handle 对象传递给任务组时,该 task_handle 对象必须保持有效,直到任务组完成为止。任务组是用来组组织、调度、等待或者取消某个或某些任务,它会将任务推入工作窃取队列, 计划程序从该队列中移除任务,并用可用的计算资源执行任务。下面示例代码并发执行若干个任务
为了方便将串行应用程序改为并行应用程序,并行模式库提供了parallel_for和parallel_for_each,我们可以很容易将自己写的 for和for_each循环代码改写为并发执行,并且不会降低代码的可读性;同样,对于管道并行,代理库(Agents Library)提供了基于数据流编程模型的C++模板库,通过使用消息传递在对象之间传输状态更改,可以隔离对共享资源的访问,从而提高可伸缩性。消息传递的优点就是它将同步绑定到数据,而不是绑定到外部的同步对象。这样就简化了组件之间的数据传输,可以消除应用程序中的编程错误。
在托管代码方面,任务并行库(Task Parallel Library)引入System.Threading.Tasks.Task类,以及Parallel.For和Parallel.ForEach来支持任务或数据并行。
此外值得一提的是PLINQ(Parallel Language-Integrated Query)。PLINQ作为LINQ的并行实现,对内存内的IEnumerable数据源进行分区,随后利用系统内的多核,并行在子数据源上操作。
无论是任务的组织、调度、执行,还是资源分配、内存共享,Visual C++ 2010为编写并发应用程序都给予了帮助,通过充分利用多核CPU性能来提升软件的性能,真正享受到亲自烹饪的性能大餐。
厨具开发工具
子曰:“工欲善其事,必先利其器”。好的开发工具是成功的一半。在Visual Studio 2010里包含了两个新的调试器窗口和一个新的性能可视化剖析器(Profiler)。有了这两个调试器窗口,我们可以在代码同等的抽象层面上也就是任务进行调试,看到任务的状态,彼此之间的关系,调用堆栈等等。当然如果感兴趣的话,也可以看到任务对应的线程,并利用一个全局的统一视图来查看所有线程的调用堆栈,以及彼此之间的关系。
图4 并行堆栈窗口
图 5 并行任务窗口
性能优化是软件开发过程中一项重要的工作,那么一个功能全面的性能探察器自然是必不可少的了。Visual Studio 2010里的并行性能可视化探查器可以看到应用程序对资源的利用情况,程序的哪个部分是受计算量限制的;也可以看到线程的执行情况,阻断的原因,线程在不同的核之间切换的情况,等等;同时还可以在线程执行的不同时间点上,跳转到相应的调用堆栈,去研究造成线程阻断的根本原因。
图6 探测器视图CPU利用率
图7 探测器视图线程
小结
我们习惯了串行思考问题,这叫思维定势,对于突如其来的并行思维,是需要一段时间才能够接受;其次,并行应用程序开发相对普通程序要难一些。改变串行(顺序)编程的这种思维方式并接受这样的编程挑战,还需要一个认识和学习的过程。就目前来说,对于大多数开发并发应用程序的程序员来说,仍是先从串行应用着手,确定了性能瓶颈后,针对关键代码段再进行并行化。有了Visual Studio 2010和.NET Framework 4这样利器,有了并行编程模型这样的好食材,编程就像烹饪,只有动手才能体会到其中的无穷乐趣,您还等什么呢?赶快行动起来吧!
原文链接:http://tech.it168.com/a2010/1215/1138/000001138613.shtml