.NET 4.5中任务并行类库的改进

日期: 2011-12-08 作者:Jonathan Allen翻译:侯伯薇 来源:TechTarget中国 英文

  微软正在努力改进.NET 4.5中应用程序的性能,特别是使用任务并行类库(Task Parallel Library)的那些应用。接下来我会带你预览将要完成的改进内容:

  Task, Task<TResult>

  .NET并行编程API的核心是Task对象。对于这样重要的类,微软想法设法保证它要尽可能小。Task的大多数属性都没有保存在类本身之中,而是保存在另一个名为ContingentProperties的对象中。这个二级对象会在程序需要的时候才创建,这样就会降低大多数一般情况下的内存占用。

  .NET 4.0发布的时候,最常见的情形是分支合并(fork-join)样式的编程,就像我们在Parallel.ForEach和Parallel LINQ中看到的那样。然而,有了.NET 4.5和其中引入的异步机制,顺序样式的编程就取而代之,占据主导地位。微软非常确信这会是主要的方式,因此他们把ContinuationObject移动到Task中,把其他字段移动到ContingentProperties中。这使得顺序结构的代码运行更快,而Task对象的规模更小。

  Task<TResult> 也避免了一些不需要的等待。它最初拥有四个属性,但是Joseph E. Hoag解释说:

  由于我们进行了一些很聪明的结构调整,结果只有m_result字段才是真正必要的。通过对已经存在于基本的Task类中的字段重新利用,我们可以废弃m_valueSelector和m_futureState字段,而存储在m_resultWasSet中的信息可以存储在基本类型的上述状态标识中。

  结果创建Task<Int32>所需的时间会减少49-55%,对象的大小会减少52%。

  Task.WaitAll, Task.WaitAny

  试想一下,我们需要同时等待十亿个任务。在一台x64的计算机上,这会导致12,000,000比特的负载,这还没有计算任务本身。如果使用.NET 4.5,负载会降到仅仅64比特。同时WaitAny的负载也会从23,200,000比特降到152比特。

  之所以出现如此戏剧化的效果,是因为微软改变了使用核心同步基元(kernel synchronization primitives)的方式。在之前的版本中,每个任务都需要一个基元(primitive )。现在已经大大减少,每个等待操作只需要一个基元,与操作中的任务数量无关。

  ConcurrentDictionary

  在.NET中,只有引用类型和很小的值类型才能够以原子的方式赋值。较大的值类型——像Guid——则无法以原子的方式读写。在.NET 4.0中,为了解决这个问题,ConcurrentDictionary会使用node对象,每次与键值关联的值发生改变的时候,都会重新创建这个对象。在.NET 4.5中,只有在无法以原子的方式对值进行写操作的时候,才会创建新的node对象。

  另一项改变是我们可以动态地创建锁。Igor Ostrovsky写到:

  在实践中,为了达到最大吞吐量,往往需要大量锁。另一方面,我们又不希望分配太多锁对象,特别是在ConcurrentDictionary最后只存储了很少项目的时候。

  想要提升性能,就要减少内存分配

  Joseph写到:

  在我们的评测结果中你可以看到,在测试中分配的内存数量和完成测试所需的时间之间有直接关系。当我们单独查看的时候,内存分配并不是非常昂贵。但是,当内存系统只是偶尔清理不使用的内存时,问题就出现了,并且问题出现的频率和要分配的内存数量成正比。因此,你分配越多的内存,对内存进行垃圾回收的频率就越频繁,你的代码性能就会变得越差。

  想要降低内存使用,一种方式就是避免使用闭包(closure)。不要在匿名的函数中捕获局部变量,我们可以把它传递给Task的构造函数,作为它的“状态(state)对象”。从.NET 4.5开始,Task.ContinueWith也会支持状态对象。

  另一种减少内存使用的技术是缓存经常使用的任务。例如,假设一个函数会接受一个数组作为参数,并返回Task<int>。因为对于空数组结果总会是一样的,所以缓存代表空数组的Task就很合理。

  下一个技巧是避免让任务不必要地“膨胀”。当某些代码触发了创建ContingentProperties的操作,Task对象就会膨胀。最经常出现的原因包括:

  创建的任务带有CancellationToken

  任务是从非默认的ExecutionContext创建的

  Task作为父Task参与到“结构化并行机制(structured parallelism)”中

  Task以Faulted状态结束

  Task通过((IAsyncResult)Task).AsyncWaitHandle.Wait()处于等待状态

  大家还要记住,任务膨胀并不一定是坏事。它只是需要注意的问题,这样我们就不会做不需要的事情,像传入从来不会用到的CancellationToken等。

我们一直都在努力坚持原创.......请不要一声不吭,就悄悄拿走。

我原创,你原创,我们的内容世界才会更加精彩!

【所有原创内容版权均属TechTarget,欢迎大家转发分享。但未经授权,严禁任何媒体(平面媒体、网络媒体、自媒体等)以及微信公众号复制、转载、摘编或以其他方式进行使用。】

微信公众号

TechTarget微信公众号二维码

TechTarget

官方微博

TechTarget中国官方微博二维码

TechTarget中国

相关推荐

  • 鲜明对比:分布式计算与并行编程

    分布式计算和并行编程的差异是一种常见的混乱,因为这两者之间有重叠的部分,也有不同的。

  • .NET变得不可变

    错误不可怕,可怕的是走进错误,但还不自知。你在编程时,是否也会走入无自知的误区呢?.NET开发中一个常见的误区,不知你是否已经走出?

  • .NET WCF 4.5 新功能介绍

    在上周华盛顿州雷德蒙德的微软开发者大会,其中一个另从期待的项目就是.NET WCF 4.5新版本,它有哪些新功能?

  • 更进一层的.Net事件跟踪机制

    .NET 4.5刚刚发布不久,新增了EventSource和EventListener两个类,为那些使用Windows事件跟踪工具来记录事件的应用程序提供了一个更加简单的编程机制。