非同步程式設計

利用著您呢 ! 暫停和播放

Torgersen mads

下載程式碼範例

在即將發行的 Visual Basic 和 C# 版本中,非同步方法是在您的非同步程式設計擺脫回呼的絕佳管道。 在本文中我將介紹靠近看哪些新等候關鍵字真正功能,在概念層級開始,我所使用熨斗向。

循序撰寫

Visual Basic 和 C# 都是命令式程式語言 — 和喝采它 ! Excel 在這表示在中可讓您用來表示您的程式設計邏輯,以個獨立的步驟系列、 趨於一個接著一個。 最陳述式層級的語言建構是控制結構,讓您以各種方式來指定要執行的程式碼指定主體個獨立的步驟順序:

  • 例如如果條件陳述式和參數可讓您選擇不同的後續動作,根據世界的目前狀態。
  • 循環播放,例如陳述式中的 foreach,同時可讓您重複一組特定步驟的執行多次。
  • 例如繼續陳述式,請擲回和移至可讓您不在本機將控制權轉移到程式的其他部分。

使用控制結構產生的程式邏輯所累積循序撰寫,它的命令性程式設計活力的泉源。 它的確為何有這麼多的控制結構,可供選擇: 您想要非常方便且組織完善的循序組字。

連續執行

在命令式語言中,包括 Visual Basic 和 C# 的目前版本的執行方法 (或函式或程序或我們稱它們所選擇的任何內容) 是連續。 我的是指的是,一旦的控制緒已經開始執行指定的方法,它將會持續佔用方法執行結束之前,執行這項作業。 是,有時候執行緒將會執行陳述式中呼叫的程式碼,你的身體的方法,但這只是一部份的執行方法。 執行您的方法不請自來的任何項目將會永遠不會切換執行緒。

此持續性有時很麻煩。 偶而沒有任何方法能如何使進度,其實一點都不是等到發生某些事情: 下載、 檔案存取、 在不同執行緒上發生的計算時間到達時] 中的某一點。 在這種情況下,執行緒被完全佔用不任何動作。 常用的名詞,是這個執行緒已經封鎖。讓它以執行這項操作的方法即為封鎖

以下是嚴重封鎖之方法的範例:

static byte[] TryFetch(string url)
{
  var client = new WebClient();
  try
  {
    return client.DownloadData(url);
  }
  catch (WebException) { }
  return null;
}

執行這個方法的執行緒仍在大部分的用戶端呼叫期間較為。DownloadData,不執行任何實際的工作,但是只等候。

當執行緒是非常寶貴,這是不正確,通常是。 在典型的中介層中,依序服務每個要求需要向後端或其他服務。 如果每個要求由它自己的執行緒來處理這些執行緒通常會封鎖等待中繼結果,大量的執行緒在中介層很容易就成為效能瓶頸。

或許最寶貴的執行緒是 UI 執行緒: 沒有只能有一個。 幾乎所有 UI 架構都是單一執行緒,以及所需的所有項目與 UI 相關 — 事件]、 [更新]、 [使用者的 UI 處理邏輯,發生同一個專用的執行緒。 如果其中一個這些活動 (例如,事件處理常式選擇從 URL 下載) 等,整個 UI 是無法製作進度,因為它的執行緒已忙於執行絕對只。

我們需要是能夠共用執行緒的多個連續活動的方法。 若要這樣做,他們需要有時候 「 取得分 」 — 也就是讓其他人可以從何處取得相同的執行緒上執行的項目在其執行漏洞。 也就是說,有時候需要將不連續。 如果這些循序活動取得該符號,它們仍然執行任何動作時,它會格外有用。 用場: 非同步程式設計!

非同步程式設計

現在,方法一定是連續的因為您有分割不連續的活動 (例如之前和之後的下載) 分成多個方法。 若要就執行方法的中間的洞裡,您必須將它清除分開成其連續的位元。 Api 可以協助所提供的長時間執行方法的初始化作業 (開始下載,例如) 非同步 (非封鎖性) 的版本,儲存執行完成時的傳入的回呼,並立即傳回給呼叫者。 但為了讓呼叫端提供回呼,之後] 的活動需要細分成不同的方法。

這適用於上述的 TryFetch 方法的方式如下:

static void TryFetchAsync(string url, Action<byte[], Exception> callback)
{
  var client = new WebClient();
  client.DownloadDataCompleted += (_, args) =>
  {
    if (args.Error == null) callback(args.Result, null);
    else if (args.Error is WebException) callback(null, null);
    else callback(null, args.Error);
  };
  client.DownloadDataAsync(new Uri(url));
}

您在這裡看到有幾種不同的傳遞回呼: DownloadDataAsync 方法必須有已註冊為 DownloadDataCompleted 事件,讓它如何通過 「 後 」 的部分方法的事件處理常式。 TryFetchAsync 本身也必須處理它的呼叫端的回呼。 而非設定該整個事件公司自己,您可以使用簡單的方法,只是進行回呼,以做為參數。 它是件好事,我們可以事件處理常式使用 lambda 運算式,以便它只是擷取並使用 「 回呼"參數直接; 如果您嘗試使用具名的方法,您必須將某種方法讓事件處理常式的回呼委派。 只是暫停一秒鐘,認為您可以撰寫而 lambda 這段程式碼的方式。

但此處需注意的重點是變更控制流程。 代替使用語言的控制結構,以表示流程,模擬它們:

  • 傳回陳述式是藉由呼叫回呼的模擬。
  • 藉由呼叫回呼被模擬隱含傳播例外狀況。
  • 例外處理的被模擬的型別檢查。

當然,這是一個非常簡單的範例。 為所需的控制項結構變得更複雜,模擬它取得更多的。

若要總而言之,我們會得到不一致,並執行的能力藉此執行緒來執行 「 等待 」 下載時的其他作業。 但我們失去使用控制結構,以表示流程的便利性。 我們為結構化的命令式語言放棄源頭。

非同步方法

當您檢視問題這種方式時,它會變成 Visual Basic 和 C# 的下一個版本的清除如何非同步方法說明: 它們可以讓您快速不連續的循序程式碼

讓我們來看非同步版本的 TryFetch 這個新的語法:

static async Task<byte[]> TryFetchAsync(string url)
{
  var client = new WebClient();
  try
  {
    return await client.DownloadDataTaskAsync(url);
  }
  catch (WebException) { }
  return null;
}

非同步方法可以讓您中斷內嵌,在您的程式碼的中間: 不僅可以表達循序撰寫使用您最愛的控制結構,您也可以使用執行中的 poke 漏洞所等候的運算式,其中執行的執行緒是可用來進行其他動作的漏洞。

想想這個問題的好方法,是假設非同步方法有 「 暫停 」 和 「 播放 」 按鈕。 在執行中執行緒達到著您呢 ! 運算式時,叫 「 暫停 」 按鈕,然後方法執行已暫停。 正在 awaited 工作完成時,叫 「 播放 」 按鈕,並會繼續執行方法。

編譯器重寫

當複雜項目看起來簡單時,這通常表示有是有趣著手蒙面客,和值得與非同步方法的情況。 簡化為您提供不錯的抽象概念,因此更方便同時寫入和讀取非同步程式碼。 了解狀況下並非必要。 但如果您知道,就能一定協助您變得更好非同步程式設計,而且能夠完整利用的功能。 而且,如果您在閱讀本,多半就也只是一般問題。 我們親自: 非同步方法做什麼 — 和 await 運算式中的,實際執行?

當 Visual Basic 或 C# 編譯器會取得的非同步方法時,它 mangles 它稍微在編譯期間: 不直接支援的基礎執行階段不一致的方法,以及必須模擬由編譯器。 因此您不必將方法較拉到位元,而不是,編譯器會為您。 不過,它會比您就應該手動操作完全不同。

編譯器會為您非同步方法 statemachine。 您的執行中的位置,以及您的本機狀態為何,都會追蹤的狀態機器。 它可以是執行暫止。 當它執行時,它可能會碰到結果 「 暫停 」 按鈕,並且會暫停程式執行的 await。 當它已暫停時,項目可能會叫用 「 播放 」 按鈕,才能執行,讓它。

運算式負責設定項目,讓 awaited 的任務完成時,取得推入 「 播放 」 按鈕。 著您呢 ! 我們的之前,不過,我們看狀態機器本身,以及有這些 [暫停] 和 [播放] 按鈕真的。

任務產生器

非同步方法產生的工作。 更具體地說,非同步方法傳回一種類型的任務或工作 <T> 執行的個體從 System.Threading.Tasks,且會自動產生執行個體。 它不一定要 (和不能) 提供的使用者程式碼。 (這是小型的 lie: 非同步方法可以傳回 void,但我們會忽略的暫時。)

編譯器的觀點來看,從產生的工作是最簡單的部份。 它會仰賴工作建立幫手] (因為它通常不會直接人類) 位於 System.Runtime.CompilerServices 的架構提供概念。 比方說,是一個型別,就像這樣:

public class AsyncTaskMethodBuilder<TResult>
{
  public Task<TResult> Task { get; }
  public void SetResult(TResult result);
  public void SetException(Exception exception);
}

[建立幫手] 可讓取得一個任務,編譯器,並可讓它完成任務的結果或例外狀況。 圖 1 是輪廓此機器的 TryFetchAsync 的外觀。

圖 1 建立工作

static Task<byte[]> TryFetchAsync(string url)
{
  var __builder = new AsyncTaskMethodBuilder<byte[]>();
  ...
Action __moveNext = delegate
  {
    try
    {
      ...
return;
      ...
__builder.SetResult(…);
      ...
}
    catch (Exception exception)
    {
      __builder.SetException(exception);
    }
  };
  __moveNext();
  return __builder.Task;
}

謹慎地觀察:

  • 首先會建立幫手。
  • 然後會建立 __moveNext 委派。 這個委派會是 「 播放 」 按鈕。 我們稱它恢復委派,且包含:
    • (雖然我們有目前 elided) 的原始程式碼從您的非同步方法。
    • 傳回陳述式,用以代表推入 「 暫停 」 按鈕。
    • [建立幫手] 呼叫完成成功的結果,使用對應到原始的程式碼的傳回陳述式。
    • 文繞圖 try/catch 完成 [建立幫手],且任何逸出的例外狀況。
  • 現在是按下 「 播放 」 按鈕。恢復委派會呼叫。 它執行,直到碰到 「 暫停 」 按鈕。
  • 工作會傳回給呼叫者。

任務產生器都是僅供編譯器使用的特殊的協助程式型別。 不過,它們的行為不是與當您直接使用 TaskCompletionSource 類型的工作平行程式庫 (TPL) 時,會發生什麼事。

到目前為止,我已經建立要傳回的工作和 「 播放 」 按鈕-再生委派-的人來呼叫時便可開始繼續執行。 我仍需要,請參閱如何繼續執行,和如何著您呢 ! 運算式設定若要這樣做某件事。 我全部整合使用之前,不過,讓我們看看如何使用工作。

Awaitables 和 Awaiters

如您所見,就可以 awaited 工作。 基本和但是,Visual C# 是非常滿意於等候其他動作,只要它們 awaitable。也就是說,只要有著您呢 ! 運算式可以對編譯的特定圖形。 為了要讓 awaitable,項目必須有 GetAwaiter 方法,它會傳回 awaiter。 例如,工作 <TResult> 具有 GetAwaiter 方法會傳回這個型別:

public struct TaskAwaiter<TResult>
{
  public bool IsCompleted { get; }
  public void OnCompleted(Action continuation);
  public TResult GetResult();
}

在 awaiter 上的成員會讓編譯器檢查 awaitable 是否已經完成,如果是,不是註冊的回呼至它取得結果的例外狀況) 時。

我們現在可以開始著您呢 ! 應該怎麼做才能暫停和繼續周圍 awaitable,請參閱。 比方說,在本例中 TryFetchAsync 著您呢 ! 會變成像這樣:

 

__awaiter1 = client.DownloadDataTaskAsync(url).GetAwaiter();
  if (!__awaiter1.IsCompleted) {
    ...
// Prepare for resumption at Resume1
    __awaiter1.OnCompleted(__moveNext);
    return; // Hit the "pause" button
  }
Resume1:
  ...
__awaiter1.GetResult()) ...

同樣地,監看會發生什麼事:

  • Awaiter 會取得 DownloadDataTaskAsync 所傳回的工作。
  • 如果 awaiter 並不完整,「 播放 」 按鈕,再生委派,會傳遞至回呼為 awaiter。
  • 當 awaiter 恢復執行 (在 Resume1) 是取得結果,並將其用於緊接在後的程式碼。

很顯然常見的情況是 awaitable 是任務或工作 <T>。 事實上,這些型別,其中已存在的 microsoft。NET Framework 4-深切最佳化這個角色。 不過,有充分的原因,可以讓其他 awaitable 的類型:

  • 其他技術來橋接: F#,比方說,具有型別非同步 <T> 通常會對應 Func < 工作 <T> >。 能夠以非同步 <T> 著您呢 ! 直接從 Visual Basic 和 C# 協助非同步以兩種語言撰寫的程式碼之間的橋樑。 F# 同樣的公開橋接功能,以其他方式,直接在非同步 F# 程式碼中使用的工作。
  • 實作特殊的語意: TPL 本身加入這幾個簡單的範例。 靜態 Task.Yield 公用程式方法,比方說,會傳回 awaitable 會宣告 (透過 IsCompleted) 不完整,但如同完成事實上立即將排定傳遞至其 OnCompleted 方法,回撥。 這可讓您強制執行排程,並略過它略過如果結果可使用的編譯器的最佳化。 這可用於就 「 現用 」 的程式碼中的漏洞和加強的程式碼,不坐在閒置的回應。 工作本身無法表示已完成,但是宣告沒有,因此,使用特殊的 awaitable 型別。

我看進一步的工作 awaitable 實作之前,讓我們完成查看編譯器的重新寫入的非同步方法,並實現記錄之追蹤的方法的執行狀態。

狀態機器

若要將它放在一起,我需要建置狀態機器生產和消耗的工作。 基本上,從原始方法的所有使用者邏輯會都放入再生委派,但讓它們可以不受影響多個引動過程出被提取的區域變數宣告。 此外,追蹤多久了解事項,導入的狀態變數,並恢復委派的使用者邏輯包裝大交換器的狀態,並跳至對應的標籤中。 因此呼叫恢復時,它會跳馬上回來,到最後一次停止的地方。 圖 2 將整個圖表放在一起。

圖 2] 建立狀態機器

static Task<byte[]> TryFetchAsync(string url)
{
  var __builder = new AsyncTaskMethodBuilder<byte[]>();
  int __state = 0;
  Action __moveNext = null;
  TaskAwaiter<byte[]> __awaiter1;
 
  WebClient client = null;
 
  __moveNext = delegate
  {
    try
    {
      if (__state == 1) goto Resume1;
      client = new WebClient();
      try
      {
        __awaiter1 = client.DownloadDataTaskAsync(url).GetAwaiter();
        if (!__awaiter1.IsCompleted) {
          __state = 1;
          __awaiter1.OnCompleted(__moveNext);
          return;
        }
        Resume1:
        __builder.SetResult(__awaiter1.GetResult());
      }
      catch (WebException) { }
      __builder.SetResult(null);
    }
    catch (Exception exception)
    {
      __builder.SetException(exception);
    }
  };
 
  __moveNext();
  return __builder.Task;
}

很 mouthful! 我相信您要求自己為什麼這段程式碼會更詳細的資訊比前面所示的手動 「 非同步的 「 版本。。 有幾個很好的理由,包括效率 (較少配置,在一般情況下) 與上述 (它會套用到使用者定義的 awaitables,不只是工作)。 不過,主要的原因如下: 您沒有提取使用者邏輯分開畢竟; 您只要擴增與某些跳躍點,並傳回等等。

雖然此範例是太容易真正左右對齊,重新方法的邏輯寫入語意對等的一組特定方法之間的邏輯其連續位元的每個正等著是非常複雜的商務。 更多的控制結構正等著它取得更糟、,在巢狀。 當不只是迴圈繼續] 和 [中斷的陳述式,但請試著最後區塊與甚至到陳述式放在正等著,它會變得非常困難,如果真的有可能,產生與高畫質經過重寫。

它看起來的嘗試,,而不是一個很棒的技巧是只是重疊顯示使用者的另一層的控制結構,您在 airlifting (有條件的跳躍點) 的原始程式碼和輸出 (與傳回),因為這種情況需要。 播放和暫停。 在 Microsoft,我們已經已有系統地測試同步相對應,非同步方法的功能相同,我們已經確認這是非常強大的方法]。 保留的程式碼,一開始就說明這些語意,同步的語意保留比非同步領域沒有更好的方式。

小號字體印刷

我所提供的說明有點 idealized,有一些更多的技巧,以重新寫入,您可能會懷疑。 以下是問題的幾個其他編譯器必須將處理:

移至陳述式 中的重新寫入 圖 2 並不會實際編譯,因為到陳述式 (在 C# 中至少) 無法跳到埋在巢狀結構中的標籤。 編譯器會產生中繼語言 (IL),不是來源程式碼,並不先由巢狀結構,這會是本身沒有問題。 但即使 IL 不允許跳到中間的,請嘗試 [我的範例。 區塊中,如同您在 相反地,實際上是跳至嘗試區塊的開頭、 通常輸入它,然後切換,跳一次。

最後會封鎖時傳回超出再生委派,因為著您呢 ! 的您不想最後組織尚未執行。 何時應該儲存原始傳回陳述式,從使用者程式碼會執行。 您控制會產生布林旗標,表示是否最後主體應該會執行,並擴大,檢查它。

評估順序運算式不一定是第一個引數之方法或運算子。 著您呢 !它可能會發生在中間。 若要保留的評估順序,所有先前的引數必須評估再 await,並儲存它們,一次擷取它們,await 是令人意外的相關資訊之後的動作。

在頂端,有一些限制,您無法取得周圍。 比方說,正等著不被允許的一點要注意內或區塊,因為我們不知道若要重新建立在最後等後的正確例外狀況內容的好方法。

工作 Awaiter

編譯器產生的程式碼用來實作著您呢 ! 運算式 awaiter 有相當大自由做為它排定再生委派的方式-也就是非同步方法的其餘部分。 不過,案例會有真正前進之前您必須實作自己的 awaiter。 工作本身有很相當多的彈性,他們排定因為它們遵守排程內容本身是隨插即用的概念。

排程的內容是其中一個可能看起來有點之前,如果我們必須從一開始為其設計這些概念。 是,它是 amalgam 的幾個現有我們決定不要更動的概念最多進一步嘗試引入一個統一的概念,在最上面。 讓我們看看在概念層級中,這個概念,我將在再深入解析。

基礎的非同步回呼,awaited 的任務排程的原理是您想要繼續執行 「 原先,"某些值的 「 位置 」。這是它 」 的 「 我呼叫的排程內容。 排程的內容是執行緒仿射的概念。每個執行緒都 (至少) 一。 當您正在執行的執行緒上時,您可以要求排程內容中,執行和排程的內容後,您可以排定執行中的項目。

所以這不是非同步的方法應該採取什麼動作時它正等著一項工作:

  • 在暫止: 要求的執行緒上執行的排程內容。
  • 在恢復: 再生委派打開該排程內容的排程。

為何這很重要? 請考慮在 UI 執行緒。 它有它自己排程的內容,以透過訊息佇列 UI 執行緒上重新加以傳送,排定新的工作。 這表示如果您正在執行 UI 執行緒上,而在一個任務,等準備工作的結果時,非同步方法的其餘部分將會執行上一步 UI 執行緒上。 因此,您可以只在 UI 執行緒 (操作 UI) 的所有項目仍可進行後 await。在您的程式碼的中間,您將不會遇到奇怪吧 」 執行緒躍點 」。

其他排程的內容都是多執行緒。明確地說,標準的執行緒集區會以單一的排程內容。 當它排定新的工作時,它可能會在任何集區的執行緒上。 因此,一個非同步方法,在執行緒集區上執行一開始會繼續執行這項操作,雖然躍它可能會 「 點周圍 」 不同的特定執行緒之間。

在練習中,對應的排程內容沒有單一的概念。 大致來說,執行緒的 SynchronizationContext 是做為其排程的內容。 因此,如果執行緒有其中之一所 (現有概念,可以是使用者執行),就會使用。 如果沒有,則會使用執行緒的 TaskScheduler (由 TPL 引入的類似概念)。 如果沒有這些其中之一,就會使用預設 TaskScheduler。 該有人同時排定 resumptions 標準的執行緒集區。

當然,所有這個排程的業務蒙受效能成本。 通常,在使用者的情況下,且可以忽略也值得: UI 程式碼中切碎到可管理的位元的實際的實際工時,並隨著等待的結果,在提取透過訊息幫浦是通常只不差。

有時候,雖然-尤其是在程式庫程式碼-得太細微的項目。 請考慮:

async Task<int> GetAreaAsync()
{
  return await GetXAsync() * await GetYAsync();
}

這兩次排程回到排程的內容-每個著您呢 ! 之後,只是為了在 「 右邊 」 的執行緒上執行乘法。 但誰在乎您要相乘的哪些執行緒呢? 這是可能浪費 (如果常使用),而且有技巧可以避免: 可以基本上使 awaited 的任務在非工作 awaitable,知道如何關閉排程後行為,並只在無論使用何種執行緒完成工作,執行恢復避免內容切換和排程的延遲:

async Task<int> GetAreaAsync()
{
  return await GetXAsync().ConfigureAwait(continueOnCapturedContext: false)
    * await GetYAsync().ConfigureAwait(continueOnCapturedContext: false);
}

較少美觀,為了安全起見,但絕招最後瓶頸的排程問題的程式庫程式碼中使用。

請 Forth 和 Async'ify

現在您應該瞭解的非同步方法 underpinnings 的工作。 可能會用掉最有用的指向是:

  • 編譯器會實際保留您的控制結構,以保留您的控制結構的意義。
  • 非同步方法不要排程新的執行緒,它們可以讓您在現有的 multiplex。
  • 當工作取得 awaited 時,它們讓您上一步 」,您是 「 合理的項目定義的表示。

如果您像我一樣,您已已經被間交替讀取這份文件,並輸入一些程式碼。 您已經多工處理控制項的多個流程 — 讀取和撰寫程式碼,在相同執行緒上: 您。 這是非同步方法只是讓您執行。

Mads Torgersen 是主要的經理 C# 和 Visual Basic 語言小組在 Microsoft。

感謝給下列技術專家來檢閱這份文件: 史 Toub