.NET 4.5中任務并行類庫的改進

fmms 13年前發布 | 6K 次閱讀 .NET

微軟正在努力改進 .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 等。

查看英文原文:Task Parallel Library Improvements in .NET 4.5

     來自: InfoQ
 本文由用戶 fmms 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!