非同步程式設計

非同步效能: 了解非同步的成本和著您呢 !

史 Toub

 

非同步程式設計向來只有最技能和 masochistic 的開發人員的領域,有的時間、 inclination 和原因相關的非線性的控制流程的回呼之後的回撥的心理容量。 Microsoft。NET 架構 4.5、 C# 及 Visual Basic 提供 asynchronicity 的其他人,如此只凡人幾乎一樣容易撰寫同步的方法可以撰寫非同步方法。 沒有更多的回呼。 更明確封送處理到另一個同步處理內容的程式碼。 否無須擔心流動的結果或例外狀況。 現有的語言功能,以便於非同步開發沒有更多技巧。 contort 簡單來說,最多 hassle。

本課程中,而現在很容易著手撰寫非同步方法的 (請參閱文件的 Eric LippertMads Torgersen 本期的 MSDN 雜誌),這麼做其實也仍然需要瞭解幕後發生。 任何一次一種語言或架構提升了層級的抽象概念的開發人員可以設計程式,它總是也封裝 [隱藏性的效能成本。 在許多情況下,這類的成本是微不足道,並可以和應忽略的開發人員實作案例很多很多。 不過,它仍然更進階的開發人員了解哪種成本存在,所以它們可以執行任何必要的步驟,以避免這些成本,如果執行最後會成為可見。 behooves 就是這種與 C# 和 Visual Basic 中的非同步方法功能的情況。

在本文中我打算探討各個細節的非同步方法,提供您穩固了解如何非同步方法的實作實際上並討論一些其他 nuanced 相關的成本。 請注意這項資訊不是可讓您可讀的程式碼將無法維護的項目,所有在微最佳化和效能的名稱。 contort 它只是為您提供資訊協助您診斷任何問題,您就可以執行跨,以及提供一組工具,來幫助您克服這種潛在的問題。 同時也請注意這份文件為基礎的試用版本。NET 架構 4.5,和它的可能特定的實作詳細資料會變成之前的最終發行版本。

取得右心理模型

十年,開發人員使用 C#、 視覺 Basic、 F# 和 c + + 的高階語言來開發有效率的應用程式。 此經驗相關的各種作業,成本的相關通知這些開發人員和開發的最佳作法通知了這個認知。 例如,大部分使用情況下,呼叫同步方法是相當便宜,編譯器可以內嵌到呼叫站台被呼叫端時,更是。 因此,開發人員學習重整程式碼分成小、 容易維護的方法,通常不需要考慮任何負數的細節,從更高的方法引動過程的計數。 這些開發人員有心理模型是什麼意思呼叫方法。

非同步方法的簡介,便需要新的心理模型。 C# 和 Visual Basic 的語言和編譯器時可以提供非同步方法的視覺效果正如同其同步的對應項目,實際上是這類。 編譯器最後產生的程式碼代替程式開發人員、 程式碼類似於實作 asynchronicity 的 yore 天內的開發人員會發現有寫入,並以手動方式維護的照本宣科程式碼的數量太多。 編譯器產生的程式碼更厲害,呼叫程式庫中的程式碼。Net 中,一次會增加開發人員的身份完成的工作。 若要取得右心理模型中,若要使用該心理模型做出適當的開發,會將它一定要了解編譯器會產生您的名義。

考慮 Chunky、 不頻繁

當使用同步程式碼,具有空白主體的方法是實際上可用的。 這不是非同步方法的情況。 請考慮下列的非同步方法,以其主體中具有單一陳述式 (和正等著哪一個因缺乏最後會以同步方式執行):

public static async Task SimpleBodyAsync() {
  Console.WriteLine("Hello, Async World!");
}

中繼語言 (IL) decompiler 將會顯示一次編譯輸出類似於在顯示的內容與這個函式的本質圖 1。 什麼是簡單的單行指令碼已分成兩個方法,其中一種協助程式狀態機器類別存在於擴充。 首先是具有相同基本的簽章,為開發人員所撰寫的虛設常式方法 (相同名稱的方法有相同的可視性、 它所接受相同的參數,它會保留其傳回型別),但該虛設常式不包含任何的開發人員所撰寫的程式碼。 相反地,它包含安裝程式重複使用。 安裝程式碼初始化的狀態電腦已用來表示非同步方法,並再逐一它使用狀態機器上的第二個 MoveNext 方法的呼叫。 此狀態的電腦類型保留狀態在非同步方法,讓該狀態,以橫跨非同步而保留下來等點,如有必要。 它也包含方法主體的使用者,撰寫的但 contorted 的方式,可讓結果和例外狀況,將會消除傳回的工作。維護操作的方法中的目前位置的該執行可能會在該位置後繼續的 await。等等。

圖 1 非同步方法未定案

[DebuggerStepThrough]     
public static Task SimpleBodyAsync() {
  <SimpleBodyAsync>d__0 d__ = new <SimpleBodyAsync>d__0();
  d__.<>t__builder = AsyncTaskMethodBuilder.Create();
  d__.MoveNext();
  return d__.<>t__builder.Task;
}
 
[CompilerGenerated]
[StructLayout(LayoutKind.Sequential)]
private struct <SimpleBodyAsync>d__0 : <>t__IStateMachine {
  private int <>1__state;
  public AsyncTaskMethodBuilder <>t__builder;
  public Action <>t__MoveNextDelegate;
 
  public void MoveNext() {
    try {
      if (this.<>1__state == -1) return;
      Console.WriteLine("Hello, Async World!");
    }
    catch (Exception e) {
      this.<>1__state = -1;
      this.<>t__builder.SetException(e);
      return;
    }
 
    this.<>1__state = -1;
    this.<>t__builder.SetResult();
  }
 
  ...
}

當想要叫用何種非同步方法成本,請注意這個重複使用。 MoveNext 方法的 try/catch (JIT) 區塊可能會防止它取得內嵌,只是即時編譯器,所以至少我們現在需要的方法引動過程的成本,在同步的情況下我們可能不會 (使用這種小的方法主體中)。 我們有多個呼叫架構常式 (例如 SetResult)。 而且我們有多個寫入狀態的電腦類型的欄位。 當然,我們必須權衡這對 Console.WriteLine,可能會支配所有所需的其他成本的成本 (所需的鎖定,它不會 I/O 等等)。 此外,請注意有基礎結構會為您的最佳化。 例如,狀態機器類型是結構。 書寫盤的如果這個方法需要暫停執行,因為它正等候執行個體尚未完成,而且在這個簡單的方法,它永遠不會完成,該結構只對堆積。 因此,在未定案的這個非同步方法不會造成任何配置。 編譯器和執行階段竭力一起參與基礎結構的配置數減到最少。

知道何時不使用非同步

。Net 會嘗試產生高效率的非同步實作非同步方法,套用多個最佳化。 不過,開發人員通常具備網域知識比可以用來產生給予他們為目標的一般性的會是相當危險且不智的編譯器和執行階段自動套用的最佳化。 這一點,實際上能有助於開發人員避免在某些小設定的使用情況下,特別是針對將更細緻的方式存取的程式庫方法中使用非同步方法。 一般來說,這是這種情況時已知方法實際上可能能夠以同步方式完成,因為它依賴資料已經可供使用。

設計時非同步方法,架構開發人員會花費大量時間最佳化離開物件的配置。 這是因為配置代表其中一個可能的非同步方法的基礎結構中最大的效能成本。 配置物件的動作通常是相當便宜的。 配置的物件是類似於商品,填滿您的購物車,因為它不會花費太多心思在將項目放入您的車。它就是您實際簽出您要取出電子銀包投入大量資源。 通常廉價配置時,所產生的記憶體回收集合可以是速,當應用程式的效能。 記憶體回收的動作包括掃描透過某些部份目前已配置,以及尋找不再參考的物件。 配置更多的物件,較長的時間執行所需此標記。 此外,大型配置的物件和配置,其中較多個記憶體回收需要發生的常見問題。 以這種方式,然後配置有系統的通用效果: 非同步方法所產生的多個記憶體回收,愈慢整體的程式會執行,即使微基準測試的非同步方法本身不會顯示重要的成本。

實際產生 (因為要等待尚未完成的物件) 執行的非同步方法,必須配置於方法,傳回一個工作物件,因為該任務做為這個特定的引動過程的唯一參考的非同步方法的基礎結構。 不過,可以完成許多非同步方法引動過程,而不會產生。 在這種情況下,非同步方法的基礎結構可能會傳回快取、 已完成工作,它可以重複使用來避免配置不必要工作的其中一個。 才能夠執行這項操作的狀況下,不過,例如非同步方法非泛用任務,任務 <Boolean>,或為 <TResult> 的任務其中 TResult 是參考型別,而非同步方法的結果就是 null。 而可能在未來展開這組,通常您可以更如果您正在實作作業的領域知識。

請考慮實作像 MemoryStream 一樣的型別。 MemoryStream 是衍生自資料流,因此可以覆寫資料流的新。NET 提供最佳化的實作的性質的 MemoryStream 4.5 ReadAsync、 WriteAsync 和 FlushAsync 方法。 讀取作業只會針對在記憶體緩衝區,因此只是記憶體複本,因為如果 ReadAsync 就會同步執行,也會造成較佳的效能。 實作這個步驟與非同步方法,會看起來如下所示:

public override async Task<int> ReadAsync(
  byte [] buffer, int offset, int count,
  CancellationToken cancellationToken)
{
  cancellationToken.ThrowIfCancellationRequested();
  return this.Read(buffer, offset, count);
}

可以輕易實現。 因為讀取同步呼叫,以及因為有沒有正等著在這個方法會將實際產生控制項時,所有的引動過程的 ReadAsync 會同步完成。 現在,我們來看的資料流,例如複製作業的標準的使用狀況模式:

byte [] buffer = new byte[0x1000];
int numRead;
while((numRead = await source.ReadAsync(buffer, 0, buffer.Length)) > 0) {
  await source.WriteAsync(buffer, 0, numRead);
}

請注意這裡 ReadAsync 上呼叫這個特定系列的來源資料流永遠用來在叫用相同的計數參數 (緩衝區的長度),因此就很可能是也重複的傳回值 (讀取的位元組數)。 除了在某些罕見的情況下,將會極不可能 ReadAsync 的非同步方法實作可以使用它的傳回值,快取的工作,但您可以。

請考慮重寫這個方法,如所示圖 2。 利用這個方法,其常見的使用案例的特定層面,我們現在已經可以最佳化離開配置方式,我們無法預期的基礎結構,來執行一般的路徑。 有了這個,每次呼叫 ReadAsync ReadAsync,在前一個呼叫相同的位元組數我們可以完全避免藉由傳回相同的工作,我們所傳回的前一個引動過程的 ReadAsync 方法從任何配置的負荷。 的擷取 這樣我們預期會非常快速,並重複叫用低階作業,這類最佳化可顯著的差異,尤其是在發生記憶體回收的數目。

圖 2] 的 [最佳化工作配置

private Task<int> m_lastTask;
 
public override Task<int> ReadAsync(
  byte [] buffer, int offset, int count,
  CancellationToken cancellationToken)
{
  if (cancellationToken.IsCancellationRequested) {
    var tcs = new TaskCompletionSource<int>();
    tcs.SetCanceled();
    return tcs.Task;
  }
 
  try {
      int numRead = this.Read(buffer, offset, count);
      return m_lastTask != null && numRead == m_lastTask.Result ?
m_lastTask : (m_lastTask = Task.FromResult(numRead));
  }
  catch(Exception e) {
    var tcs = new TaskCompletionSource<int>();
    tcs.SetException(e);
    return tcs.Task;
  }
}

當案例規定快取時,可能會完成相關的最佳化,以避免工作配置。 請考慮方法的目的是下載特定的 Web 網頁的內容及然後快取已成功下載的內容的未來的存取。 這類功能可能會寫入使用非同步方法,如下所示 (使用新的 System.Net.Http.dll 程式庫中。NET 4.5):

private static ConcurrentDictionary<string,string> s_urlToContents;
 
public static async Task<string> GetContentsAsync(string url)
{
  string contents;
  if (!s_urlToContents.TryGetValue(url, out contents))
  {
    var response = await new HttpClient().GetAsync(url);
    contents = response.EnsureSuccessStatusCode().Content.ReadAsString();
    s_urlToContents.TryAdd(url, contents);
  }
  return contents;
}

這是簡單的實作。 對 GetContentsAsync 的呼叫,無法滿足從快取,建構新的工作 <string> 的額外負荷,若要顯示這份下載將是微不足道,相較於網路相關的成本。 不過,其中可能從快取滿足內容的情況下,它可以代表非-微不足道成本,只是要換行,並將交回已存在的資料物件配置。

若要避免成本 (如果這樣做是為了符合效能目標),您可以重寫這個方法中所示圖 3。 我們現在有兩種方法: 同步的公用方法和公用方法將委派的非同步私用方法。 快字典現在取產生的工作,而不是它們的內容,因此可以滿足試著下載已成功下載的網頁,以簡單的字典存取傳回現有的工作。 在內部,我們也利用 ContinueWith 方法上工作,可以讓我們來完成工作之後,將工作存入字典-但這只下載成功。 當然,這段程式碼更為複雜,需要更多的想法,撰寫和維護,因此任何效能最佳化,以避免花時間,直到效能測試證明,使其複雜性讓 impactful 和必要的差異。 這類最佳化是否產生差異實際上取決於使用案例。 請準備好的表示一般的使用模式,並判斷是否下列狀況會以有意義的方式改善您的程式碼效能時,用於分析這些測試的測試套件。

圖 3 手動快取工作

private static ConcurrentDictionary<string,Task<string>> s_urlToContents;
 
public static Task<string> GetContentsAsync(string url) {
  Task<string> contents;
  if (!s_urlToContents.TryGetValue(url, out contents)) {
      contents = GetContentsAsync(url);
      contents.ContinueWith(delegate {
        s_urlToContents.TryAdd(url, contents);
      }, CancellationToken.None,
        TaskContinuationOptions.OnlyOnRanToCompletion |
          TaskContinuatOptions.ExecuteSynchronously,
        TaskScheduler.Default);
  }
  return contents;
}
 
private static async Task<string> GetContentsAsync(string url) {
  var response = await new HttpClient().GetAsync(url);
  return response.EnsureSuccessStatusCode().Content.ReadAsString();
}

另一個工作相關的最佳化,將是是否也需要從非同步方法傳回的工作。 C# 和 Visual Basic 兩者都支援非同步方法會傳回 void,在以往的任務配置方法,這種情況下建立。 因為您身為程式庫開發人員不知道是否消費者會想要等到完成該方法的程式庫公開地公開出來的非同步方法一定要撰寫傳回任務或工作 <TResult>。 不過,某些內部使用案例中,傳回 void 的非同步方法可以有他們的位置。 支援現有事件導向的環境,例如 ASP 為 void 傳回有非同步方法的主要原因。NET 和 Windows Presentation Foundation (WPF)。 輕鬆並執行按鈕處理常式、 網頁載入事件透過非同步的使用類似,以及在等。 如果您不要考慮使用非同步 void 方法時,要非常小心周圍例外處理: 逸出的非同步處理的例外狀況會造成方法泡泡縮小到任何 SynchronizationContext 是非同步 void 方法被叫用時的最新資訊。

想要的內容

中有許多類型的 「 內容 」。NET Framework: LogicalCallContext、 SynchronizationContext、 HostExecutionContext、 SecurityContext、 ExecutionContext 等等 (從您所預期的架構開發人員 monetarily incentivized 引入新的內容,但我保證我們不人海)。 這些內容部分會非常相關非同步方法,而不只,功能方面的非同步方法的效能影響。  

SynchronizationContext SynchronizationContext 努力大非同步方法。 「 同步處理內容 」 是只是抽象,透過封送處理方式專屬於特定程式庫或架構的委派引動過程的能力。 例如,WPF 提供代表 UI 執行緒發送器的 DispatcherSynchronizationContext: 張貼這個同步處理內容的委派會排入佇列的發送器在它的執行緒上執行委派。 ASP。NET 提供的 AspNetSynchronizationContext,用來確保此非同步 ASP 的處理程序時所發生的作業。NET 要求會依序執行,右 HttpContext 狀態相關聯。 等等。 所有 told,有約為 10 具體的實作方式中的 SynchronizationContext。Net 中,有些公用,一些內部。

當發生 「 正在等候工作,並提供其他 awaitable 型別。。NET Framework 中,"awaiters"的這些型別 (如 TaskAwaiter) 在發出的著您呢 ! 時擷取目前的 SynchronizationContext。 完成的 awaitable,目前的 SynchronizationContext 被捕捉,是否表示非同步方法的其餘部分註解的接續張貼到該 SynchronizationContext。 開發人員撰寫從 UI 執行緒呼叫非同步方法不需要以手動方式封送回至 UI 執行緒的引動過程處理以修改 UI 控制項: 這類封送處理會自動處理的架構基礎結構。

不幸的是,這種封送處理也會包括成本。 使用的應用程式開發人員在等實作它們的控制流程、 此自動的封送處理幾乎都是正確的方案。 不過,程式庫,通常是不同的本文。 應用程式開發人員通常需要封送這類處理因為在乎呢它執行時,能夠存取 UI 控制項,或能夠存取 HttpContext 右 asp 內容的程式碼。NET 要求。 不過,大多數的程式庫,不會這個條件約束。 因此,這個自動的封送處理通常是完全不必要的成本。 請考慮一次顯示將資料複製到另一個資料流先前的程式碼:

byte [] buffer = new byte[0x1000];
int numRead;
while((numRead = await source.ReadAsync(buffer, 0, buffer.Length)) > 0) {
  await source.WriteAsync(buffer, 0, numRead);
}

如果從 UI 執行緒叫用此複製作業時,每個 awaited 的讀取和寫入作業會強制回復至 UI 執行緒完成。 為 1 mb 的來源資料,並完成的資料流讀取及寫入以非同步方式 (這是大部分的),這表示向上的 500 個躍點從背景執行緒至 UI 執行緒。 若要解決這,任務及工作 <TResult> 型別提供 ConfigureAwait 方法。 ConfigureAwait 接受布林 continueOnCapturedContext 參數來控制這封送處理行為。 如果使用預設值為 true,則著您呢 ! 會自動完成上擷取的 SynchronizationContext 上。 如果是 false,不過,SynchronizationContext 將會被忽略,架構會試著繼續執行,無論之前的非同步作業完成。 這將加入下列資料流複製程式碼產生更有效率的版本:

byte [] buffer = new byte[0x1000];
int numRead;
while((numRead = await
  source.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) > 0) {
  await source.WriteAsync(buffer, 0, numRead).ConfigureAwait(false);
}

程式庫開發人員,這項效能影響單獨足以用來保證一定會使用 ConfigureAwait,除非它是極少數的情況下,其中有其環境的領域知識文件庫,而不必執行正確的內容的存取方法的主體。

還有另一個理由,超過的效能,使用 ConfigureAwait 程式庫程式碼中。 假設前面的程式碼,而 ConfigureAwait,不是在呼叫 CopyStreamToStreamAsync,被叫用從 WPF UI 執行緒,方法如下:

private void button1_Click(object sender, EventArgs args) {
  Stream src = …, dst = …;
  Task t = CopyStreamToStreamAsync(src, dst);
  t.Wait(); // deadlock!
}

在此,開發人員應該撰寫 button1_Click 做為非同步方法,再等候 ed 的工作,而不是使用其同步的等候方法。 等候方法具有其重要用途,但它幾乎都是用它來等待 UI 執行緒就像這樣的錯誤。 等候方法不會傳回之前完成的工作。 如果是 CopyStreamToStreamAsync,包含等待嘗試擷取的 SynchronizationContext,回傳,以及方法無法完成,除非這些文章完成 (因為文章用來處理方法的其餘部分)。 但是,這些文章不會完成,因為會處理它們的 UI 執行緒會封鎖等候呼叫中。 這是導致死結循環相依性。 CopyStreamToStreamAsync 而必須使用 ConfigureAwait(false) 已撰寫,如果有任何循環相依性和死結。

ExecutionContext ExecutionContext 是不可或缺的一部分。Net 中,但大部分的開發人員所獨有的問題不知道它的存在。 ExecutionContext 是 granddaddy 的內容,封裝多個內容 SecurityContext 和 LogicalCallContext,表示應該自動流量跨非同步點,在程式碼中的所有項目。 您已經使用 ThreadPool.QueueUserWorkItem,Task.Run、 Delegate.BeginInvoke、 Stream.BeginRead、 WebClient.DownloadStringAsync 或在架構中,任何其他非同步作業的任何時間實際上 ExecutionContext 已擷取盡可能 (透過 ExecutionContext.Capture),並處理 (透過 ExecutionContext.Run) 所提供的委派然後使用該擷取的內容。 例如,如果叫用 ThreadPool.QueueUserWorkItem 的程式碼模擬的 Windows 身分識別時,該相同的 Windows 識別會模擬才能執行 WaitCallback 委派。 如果叫用 Task.Run 的程式碼有 LogicalCallContext 到,第一次儲存資料,該相同的資料則是透過動作委派中 LogicalCallContext。 ExecutionContext 則也會被置跨工作正等著。

有多個最佳化中的架構,以避免擷取,這樣做是沒有必要的因為這樣做,可能會非常耗時時,所擷取的 ExecutionContext 下執行。 不過,模擬的 Windows 身分,或將資料儲存到 LogicalCallContext 等動作,將會阻止這些最佳化。 避免操作 ExecutionContext 的作業,例如 WindowsIdentity.Impersonate 和 CallContext.LogicalSetData,產生較佳的效能時使用非同步方法,以及一般使用非同步。

提起您的記憶體回收的方式

本機變數時,非同步方法提供很好的假象。 在同步的方法,C # 和 Visual Basic 中的本機變數是以堆疊為主,如此沒有堆積配置需要儲存這些區域變數。 不過,在非同步方法,方法的堆疊消失時的非同步方法正在暫止著您呢 ! 點。 可供方法著您呢 ! 履歷表後的資料,該資料必須儲存某處。 因此,C # 和 Visual Basic 編譯器將區域變數 「 提升 」 至狀態機器結構,然後要在第一個堆積盤的等候暫止,讓區域變數可能不在受影響的著您呢 ! 點。

稍早在本文中我會討論如何的成本和記憶體回收頻率會受配置,而回收的頻率也受配置的物件大小的物件數目。 人質被配置更多經常物件回收需要重新執行。 因此,在非同步方法,需要為堆積,經常會發生記憶體回收會消除的多個區域變數。

在撰寫本文的時間,C # 和 Visual Basic 的編譯器有時提起超過真正必要。 例如,請考慮下列的程式碼片段:

public static async Task FooAsync() {
  var dto = DateTimeOffset.Now;
  var dt  = dto.DateTime;
  await Task.Yield();
  Console.WriteLine(dt);
}

在等之後,無法完全讀取 dto 變數,因此著您呢 ! 之前寫入的值不需要在中存活下來。 著您呢 ! 然而,狀態機器類型由編譯器產生儲存區域變數仍包含 dto 參考,如所示圖 4

圖 4 本機能拿起

[StructLayout(LayoutKind.Sequential), CompilerGenerated]
private struct <FooAsync>d__0 : <>t__IStateMachine {
  private int <>1__state;
  public AsyncTaskMethodBuilder <>t__builder;
  public Action <>t__MoveNextDelegate;
 
  public DateTimeOffset <dto>5__1;
  public DateTime <dt>5__2;
  private object <>t__stack;
  private object <>t__awaiter;
 
  public void MoveNext();
  [DebuggerHidden]
  public void <>t__SetMoveNextDelegate(Action param0);
}

這一點相當真正需要該堆積物件的大小。 bloats 如果您發現記憶體回收會比預期更頻繁地發生,看看您是否真的需要所有您已編碼加入您的非同步方法的暫存變數。 本範例可以改寫以避免額外的欄位狀態機器類別上的如下所示:

public static async Task FooAsync() {
  var dt = DateTimeOffset.Now.DateTime;
  await Task.Yield();
  Console.WriteLine(dt);
}

此外,。NET 記憶體回收行程 (GC) 是危險的行程,這表示它分割成群組,稱為層代使用一組物件: 在高階角度而言,層代 0 中配置新物件],然後再升級所有回收的物件上產生 (。NET GC 目前使用層代 0、 1 和 2)。 如此更快的集合藉由使用 GC 經常收集只從已知的物件空間的子集合。 它根據其新配置的物件將也消失,而一段時間就已經存在的物件都可以繼續繞一段時間的基本概念。 這是什麼意思是,如果物件便會存在著層代 0,它可能就會得到周圍的一段時間、 持續放在系統上的壓力,其他時間。 然後,這表示我們真的想要確保當不再需要物件使用於記憶體回收。

使用上述的提升,會保持為基礎的非同步方法的執行持續期間 (只要 awaited 的物件正確維護 awaited 作業完成時會叫用委派的參考) 的類別的欄位取得升級區域變數。 在同步的方法,JIT 編譯器能夠追蹤的區域變數將無法再存取,並在這類點可以幫助略過這些變數根目錄為 GC,使得被參考的物件可收集如果四邊不參考任何其他地方。 不過,在非同步方法,還是這些區域變數會參考,這表示它們所參考的物件可能不受影響遠比如果這些假設是真實的區域變數。 如果您發現物件還剩下斐用法活著,請考慮 nulling 出參考這些物件,當您完成它們的區域變數。 同樣地,這應該只有當您發現是實際的效能問題,原因為它否則使程式碼進行不必要。 此外,C # 和 Visual Basic 的編譯器無法更新的版本或否則未來來處理多個這些案例,開發人員的代表,因此任何這類今天撰寫的程式碼有可能會變成過時未來。

避免複雜度

C# 和 Visual Basic 的編譯器很很棒的角度來看,就您可以使用正等著: 幾乎任何地方。 在等運算式可以做為組件的較大的運算式,可讓您工作 <TResult> 著您呢 ! 執行個體在位置中,您可能有其他傳回值的運算式。 例如,請考慮下列程式碼,它會傳回三個工作結果的總和:

public static async Task<int> SumAsync(
  Task<int> a, Task<int> b, Task<int> c)
{
  return Sum(await a, await b, await c);
}
 
private static int Sum(int a, int b, int c)
{
  return a + b + c;
}

C# 編譯器可讓您使用運算式做為總和函數的引數的 「 等候 b 」。。 不過,那里會多這裡等待加總,以及因為評估規則的順序,其結果會當做參數傳遞,編譯器,這個範例中實作非同步的方式需要編譯器"散落"暫時結果的前兩個正等著。 如先前所見,區域變數會保留跨在點等是讓其提昇的狀態機器類別上的欄位。 不過,案例,其中的值是 CLR 評估堆疊,這些值不提昇至狀態機器,但改溢出到單一的暫存物件而且然後參考狀態機器。 當您完成第一項工作] 及 [跳至第二個著您呢 ! 著您呢 ! 盤的時,編譯器會產生方塊的第一個結果,並將的物件儲存成單一的 < > t__stack 欄位狀態機器上的程式碼。 當您完成的第二個任務] 和 [移至所等候的第三個著您呢 ! 時,編譯器會產生程式碼會建立一個有序元組 < int、 int > 在相同的 < > __stack 欄位中儲存的有序元組的前兩個值。 這一切表示取決於如何撰寫程式碼,您可能會收到含有非常不同的配置模式。 請考慮改撰寫 SumAsync,如下所示:

public static async Task<int> SumAsync(
  Task<int> a, Task<int> b, Task<int> c)
{
  int ra = await a;
  int rb = await b;
  int rc = await c;
  return Sum(ra, rb, rc);
}

這項變更,編譯器現在會發出至狀態機器類別來儲存 ra、 rb 和 rc,三個多個欄位,就會發生沒有溢出。 因此,您必須要付出代價: 更多的狀態機器類別具有較少的配置或更多的配置具有較小的狀態機器類別。 每個配置的物件有它自己的記憶體額外負荷,但在結束效能測試無法顯示,最好還是配置的記憶體總量會在將的情況下,較大。 一般而言,如先前所述,您不應該仔細思考這幾種微最佳化除非您發現配置是實際的困擾,原因,但是無論如何,最好知道這些配置來自何方。

當然,沒有可說更大的成本之前的範例您應該注意的並主動考慮。 要叫用的程式碼無法完成加總,直到所有三個正等著,並在中間完成任何工作正等著。 每一種正等著的會產生需要相當的工作量,正等著存貨數量越少,因此您需要處理,越好。 它會 behoove,然後將結合所有三個記錄等待成只是一個等候所有使用 Task.WhenAll 一次工作:

public static async Task<int> SumAsync(
  Task<int> a, Task<int> b, Task<int> c)
{
  int [] results = await Task.WhenAll(a, b, c);
  return Sum(results[0], results[1], results[2]);
}

這裡的 Task.WhenAll 方法會傳回工作 < [TResult] > 將不會完成直到所有提供的工作完成後,它不會這麼多效率比只等候每個個別的工作。 它也會從每個任務中收集的結果,並將它儲存到陣列。 如果您想要避免該陣列,也可以強制繫結至非泛用 WhenAll 方法,可以使用的工作 <TResult> 而不是工作執行。 最終的效能,您也可以也採用混合式的方法,讓您先檢查是否所有的任務順利完成,,如果有,取得他們的 resultsindividually,但如果沒有,,請等候 WhenAll 所沒有。 將不必要的例如配置參數陣列傳遞至方法時,它可以避免參與 WhenAll 的呼叫的任何配置。 而且,如先前所述,我們希望此程式庫函式,也能顯藏封送處理內容。 這種解決方案會顯示在圖 5

[圖 5 套用多個最佳化

public static Task<int> SumAsync(
  Task<int> a, Task<int> b, Task<int> c)
{
  return (a.Status == TaskStatus.RanToCompletion &&
          b.Status == TaskStatus.RanToCompletion &&
          c.Status == TaskStatus.RanToCompletion) ?
Task.FromResult(Sum(a.Result, b.Result, c.Result)) :
    SumAsyncInternal(a, b, c);
}
 
private static async Task<int> SumAsyncInternal(
  Task<int> a, Task<int> b, Task<int> c)
{
  await Task.WhenAll((Task)a, b, c).ConfigureAwait(false);
  return Sum(a.Result, b.Result, c.Result);
}

Asynchronicity 和效能

非同步方法是功能強大的產能工具,讓您更容易撰寫可擴充且有回應的程式庫和應用程式。 請務必要牢記在心,不過,該 asynchronicity 不是個別的作業的效能最佳化。 會進行同步作業,並將之非同步會確保降低該一個作業的效能,仍需完成同步作業一樣,但現在有額外的條件約束和考量的所有項目。 然後關心 asynchronicity,原因是效能在 aggregate: 當您撰寫的所有項目以非同步的方式,讓您可以重疊的 I/O 及達成較佳的系統利用率耗用寶貴的資源,實際上需要執行時,才整體系統的方式執行。 提供的非同步方法實作。Net 是井 optimized,而常導致提供良好或更高的效能,比撰寫良好的非同步實作,使用現有的模式和磁碟區更多的程式碼。 每當您打算開發中的非同步程式碼。NET 的架構,現在,非同步方法應該是您所選擇的工具。 然而,它是適用於您要注意的所有項目架構開發人員負責代表您在這些非同步方法,讓您可以確保最後結果是可能的期望。

史 Toub 是主要的架構設計人員在平行運算平台小組在 Microsoft。

感謝到下列的技術專家來檢閱這份文件: Joe HoagEric LippertDanny ShihMads Torgersen