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

fmms 13年前
     <div id="news_body">     <p> 微软正在努力改进 .NET 4.5 中应用程序的性能,特别是使用任务并行类库(Task Parallel Library)的那些应用。接下来我会带你预览将要完成的改进内容:</p>     <p> <strong>Task, Task<TResult> </strong></p>     <p> .NET 并行编程 API 的核心是 Task 对象。对于这样重要的类,微软想法设法保证它要尽可能小。Task 的大多数属性都没有保存在类本身之中,而是保存在另一个名为 ContingentProperties 的对象中。这个二级对象会在程序需要的时候才创建,这样就会降低大多数一般情况下的内存占用。</p>     <p> .NET 4.0 发布的时候,最常见的情形是分支合并(fork-join)样式的编程,就像我们在 Parallel.ForEach 和 Parallel LINQ 中看到的那样。然而,有了 .NET 4.5 和其中引入的异步机制,顺序样式的编程就取而代之,占据主导地位。微软非常确信这会是主要的方式,因此他们把 ContinuationObject 移动到 Task 中,把其他字段移动到 ContingentProperties 中。这使得顺序结构的代码运行更快,而 Task 对象的规模更小。</p>     <p> Task<TResult> 也避免了一些不需要的等待。它最初拥有四个属性,但是 <a href="/misc/goto?guid=4958317079699924564">Joseph E. Hoag 解释说</a>:</p>     <blockquote>      <p>由于我们进行了一些很聪明的结构调整,结果只有m_result 字段才是真正必要的。通过对已经存在于基本的 Task 类中的字段重新利用,我们可以废弃m_valueSelector 和m_futureState 字段,而存储在m_resultWasSet 中的信息可以存储在基本类型的上述状态标识中。</p>     </blockquote>     <p> 结果创建 Task<Int32>所需的时间会减少 49-55%,对象的大小会减少 52%。</p>     <p> <strong>Task.WaitAll, Task.WaitAny</strong></p>     <p> 试想一下,我们需要同时等待十亿个任务。在一台 x64 的计算机上,这会导致 12,000,000比特的负载,这还没有计算任务本身。如果使用 .NET 4.5,负载会降到仅仅 64 比特。同时 WaitAny 的负载也会从 23,200,000比特降到 152 比特。</p>     <p> 之所以出现如此戏剧化的效果,是因为微软改变了使用核心同步基元(kernel synchronization primitives)的方式。在之前的版本中,每个任务都需要一个基元(primitive )。现在已经大大减少,每个等待操作只需要一个基元,与操作中的任务数量无关。</p>     <p> <strong>ConcurrentDictionary</strong></p>     <p> 在 .NET 中,只有引用类型和很小的值类型才能够以原子的方式赋值。较大的值类型——像 Guid——则无法以原子的方式读写。在 .NET 4.0 中,为了解决这个问题,ConcurrentDictionary 会使用 node 对象,每次与键值关联的值发生改变的时候,都会重新创建这个对象。在 .NET 4.5 中,只有在无法以原子的方式对值进行写操作的时候,才会创建新的 node 对象。</p>     <p> 另一项改变是我们可以动态地创建锁。<a href="/misc/goto?guid=4958317080490164667">Igor Ostrovsky 写到</a>:</p>     <blockquote>      <p> 在实践中,为了达到最大吞吐量,往往需要大量锁。另一方面,我们又不希望分配太多锁对象,特别是在 ConcurrentDictionary 最后只存储了很少项目的时候。</p>     </blockquote>     <p> <strong>想要提升性能,就要减少内存分配</strong></p>     <p> Joseph 写到:</p>     <p> 在我们的评测结果中你可以看到,在测试中分配的内存数量和完成测试所需的时间之间有直接关系。当我们单独查看的时候,内存分配并不是非常昂贵。但是,当内存系统只是偶尔清理不使用的内存时,问题就出现了,并且问题出现的频率和要分配的内存数量成正比。因此,你分配越多的内存,对内存进行垃圾回收的频率就越频繁,你的代码性能就会变得越差。</p>     <p> 想要降低内存使用,一种方式就是避免使用<a href="/misc/goto?guid=4958317081270092367">闭包(closure)</a>。不要在匿名的函数中捕获局部变量,我们可以把它传递给 Task 的构造函数,作为它的“状态(state)对象”。从 .NET 4.5 开始,Task.ContinueWith 也会支持状态对象。</p>     <p> 另一种减少内存使用的技术是缓存经常使用的任务。例如,假设一个函数会接受一个数组作为参数,并返回 Task<int>。因为对于空数组结果总会是一样的,所以缓存代表空数组的 Task 就很合理。</p>     <p> 下一个技巧是避免让任务不必要地“膨胀”。当某些代码触发了创建 ContingentProperties 的操作,Task 对象就会膨胀。最经常出现的原因包括:</p>     <ul>      <li>创建的任务带有 CancellationToken</li>      <li>任务是从非默认的 ExecutionContext 创建的</li>      <li>Task 作为父 Task 参与到“结构化并行机制(structured parallelism)”中</li>      <li>Task 以 Faulted 状态结束</li>      <li>Task 通过((IAsyncResult) Task) .AsyncWaitHandle.Wait ()处于等待状态</li>     </ul>     <p> 大家还要记住,任务膨胀并不一定是坏事。它只是需要注意的问题,这样我们就不会做不需要的事情,像传入从来不会用到的 CancellationToken 等。</p>     <p> <strong>查看英文原文:</strong><a href="/misc/goto?guid=4958317082055060588">Task Parallel Library Improvements in .NET 4.5</a></p>     <div id="come_from">           来自:      <a id="link_source2" href="/misc/goto?guid=4958317082834294493" target="_blank">InfoQ</a>     </div>    </div>