.NET中使用異步Async和Await的代價

看到了這樣的一篇文章,覺得寫的不錯,與大家分享:

異步技術能使得應用程序的總吞吐量得到顯著提升,但這並不是無償的。異步函數往往比其同步替代方案稍慢一些,而且如果您不介意採用它還會增加相當大的內存壓力的話。Stephen Toub最近在MSDN雜誌中一篇題爲“異步性能:瞭解Async和Await的代價”的文章中討論了該主題。

相對於本機C++代碼而言,託管代碼最顯著的優勢之一就是運行時內聯函數(inline function)[1]的能力。CLR的JIT編譯器甚至可以跨程序集內聯函數,從而大大降低了調用細粒度方法(OOP程序員偏愛此類方法)的開銷。不幸的是,異步調用的本質意味着不能內聯委託(delegates cannot be inlined)。此外,在建立異步調用時還包括不少樣板代碼。因此,這導致了Stephen的第一條建議,“考慮粗粒度,而非細粒度(Think Chunky, Not Chatty)”[2]。就像你正在穿越某個COM或p/invoke邊界一樣,相對於許多的小型異步調用而言,你應該會更喜愛少數的大型異步調用。

異步模式下無需開發者顯式使用new運算符,即可通過多種方式分配內存。如果任其發展,這些內存分配法可能導致過大的內存壓力,並且由於垃圾回收器嘗試跟進還會導致不必要的延遲。考慮來自Stream子類的這個簽名及其返回語句:

public override async Task<int> ReadAsync(…)
return this.Read(…)

此處沒有展示隱式創建Task對象,該對象用於包裝從Read方法中返回的整型值。在Stephen的文章中,他展示瞭如何通過緩存最近的Task<int>對象及重用該對象來降低內存開銷。

導致意外對象分配和保留的另一原因是使用閉包(closures)。C#和VB中的閉包是通過匿名類來實現的,匿名類包含匿名方法,而且在方法中聲明瞭異步函數。那些匿名函數所需的本地變量據說被“封閉”(closed over)或“提升” (lifted)到該匿名類中。當每次調用匿名類的父方法時都必須創建一個該類實例。

問題並未就此結束,仍有可能使得額外的內存分配進一步惡化。通常情況下,局部變量所引用的對象是被熱切請求的,垃圾回收器(GC)一旦明確那些局部變量在當前函數中將不再被使用時就會回收它們。由於在異步函數中所使用的“局部變量”實際上是某個匿名類中的字段,因此在調用期間它們必須被保留。如果此過程耗時數秒,這對於異步調用而言是很常見的,而該匿名類可能在不經意間被晉升爲垃圾回收器中更昂貴的1代或2代對象[3]。如果這成爲問題,Stephen建議一旦不再需要那些局部變量就應顯式地把它們設置爲空引用。

Stephen所討論的第三個問題是上下文的概念,特別是同步上下文(synchronization context)和執行上下文(execution context)。他在文章中展示了庫代碼如何通過使用ConfigureAwait方法故意忽略同步上下文、以及避免某些必須在執行上下文中捕獲的事情來獲得性能提升的辦法。

譯註

[1] 內聯函數(inline function),在不同的編程語言中,內聯函數(inline function)是指已要求編輯器對其執行內聯展開(inline expansion)的函數。換言之,程序員已要求編譯器將每處調用某函數的地方都插入完整的函數體,而不是生成代碼以便從其定義的地方調用該函數。可以使用C99或C++編寫內聯函數,例如:

inline int max(int a, int b)
{
  return (a > b) ? a : b;
}

然後,調用語句如下:

a = max(x, y);

該語句在編譯後,可能被轉換成爲更直接的計算:

a = (x > y) ? x : y;

詳見Inline function

[2] 考慮粗粒度,而非細粒度(Think Chunky, Not Chatty),Chunky與Chatty之爭此前多見於“服務協定設計”(service contract design)。嘮叨的服務(Chatty Service)趨向於返回簡化信息,並使用更細粒度的操作。矮胖的服務(Chunky Service)趨向於返回複雜層次信息,並使用粗粒度的操作。換言之,二者不同之處在於,當返回同樣的信息時,嘮叨的服務與矮胖的服務相比則需要更多的調用,卻增加了返回實際需要的適當信息的靈活性。詳見WCF service contract design

[3] 1代或2代對象,“代”是垃圾回收器用到的概念。提到垃圾回收器就不得不說“託管堆的簡化模型”,該模型的規則如下:

  • 所有可進行垃圾回收的對象都分配在一個連續的地址空間範圍(託管堆)內。
  • 堆被劃分爲代 (generation),以便只需查找堆的一小部分就能清除大多數垃圾。
  • 代中的對象大體上均爲同齡。
  • 代的編號越高,表示堆的這一片區域所包含的對象越老——這些對象就越有可能是穩定的。最老的對象位於最低的地址內,而新的對象則創建在增加的地址內。
  • 新對象的分配指針標記了內存的已使用(已分配)內存區域和未使用(可用)內存區域之間的邊界。
  • 通過刪除死對象並將活對象轉移到堆的低地址末尾,堆週期性地進行壓縮。這就擴展了在創建新對象的圖表底部的未使用區域。
  • 對象在內存中的順序仍然是創建它們的順序,以便於定位。
  • 在堆中,對象之間永遠不會有任何空隙。
  • 只有某些可用空間是已提交的。需要時,操作系統會從“保留的”地址範圍中分配更多的內存。

詳見“垃圾回收器基礎與性能提示”

查看英文原文:The Cost of Async and Await

譯者 高翌翔 基於.NET平臺進行Web應用程序設計、開發,關注敏捷開發和架構設計,及各種提高代碼可維護性的最佳實踐。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章