到 2015 2015年 7 月

30 卷數 7

非同步程式設計-棕非同步開發

Stephen Cleary |到 2015 2015年 7 月

Visual Studio非同步 CTP 出來時,我卻在幸運的位置。我是唯一開發商兩個相對較小的綠地應用程式將受益于非同步和等待。在此期間,各成員的 MSDN 論壇包括自己被發現、 討論和實施幾個非同步最佳做法。其中最重要的這些做法都編譯到 2013 年 3 月我 MSDN Magazinearticle、"最佳做法在非同步程式設計"(msdn.microsoft.com/magazine/jj991977)。

應用非同步和等待到現有的代碼基是一種不同的挑戰。棕地代碼可以是混亂,這使情況進一步複雜化。將非同步應用於棕代碼我會解釋在這裡發現了有用的幾個技術。介紹非同步實際上可以影響在某些情況下的設計。如果有任何重構的必要將現有代碼分離成層,我建議做,在引入非同步前。對於這篇文章的目的,我會假設你正在使用的應用程式體系結構類似于所示圖 1

圖 1 簡單的代碼結構與服務層和業務邏輯層

public interface IDataService
{
  Task<string> GetAsync(int id);
}
public sealed class WebDataService : IDataService
{
  public async Task<string> GetAsync(int id)
  {
    using (var client = new WebClient())
      return await client.DownloadStringTaskAsync(
      "http://www.example.com/api/values/" + id);
  }
}
public sealed class BusinessLogic
{
  private readonly IDataService _dataService;
  public BusinessLogic(IDataService dataService)
  {
    _dataService = dataService;
  }
  public async Task<string> GetFrobAsync()
  {
    // Try to get the new frob id.
    var result = await _dataService.GetAsync(17);
    if (result != string.Empty)
      return result;
    // If the new one isn't defined, get the old one.
    return await _dataService.GetAsync(13);
  }
}

何時使用非同步

最好的一般方法是應用程式實際上是第一次思考。非同步擅長 O 綁定操作,但有時會有更好的選擇,對於其他種類的加工。有兩種常見有點方案在非同步已不是一個完美的結合 — — CPU 綁定代碼和資料流程。

如果你有 CPU 綁定代碼,考慮並行類或並行LINQ。非同步是更適合於基於事件的系統,那裡有沒有實際的代碼執行的操作正在進行時。非同步方法內的 CPU 綁定代碼仍將同步運行。

然而,你可以對待 CPU 綁定代碼,好像它被非同步等待 Task.Run 的結果。 這是很好地推掉 UI 執行緒的 cpu 的工作。下面的代碼是使用 Task.Run 作為非同步和並行代碼之間的橋樑的示例:

await Task.Run(() => Parallel.ForEach(...));

在非同步不是最適合另一種情況是在您的應用程式處理資料流程時。非同步作業有確定的開始和結束。例如,資源下載時啟動請求的資源。它完成資源下載完成時。如果你傳入的資料更多的流或訂閱,非同步可能不是最好的辦法。考慮到可能在任何時間,如志願者資料的序列埠連接的設備。

它是可能用於等待非同步/事件流。它將緩衝資料,當它到達直到應用程式讀取資料需要一些系統資源。如果您的源事件訂閱,請考慮使用無功擴展或協力廠商物流資料流程。你可能會發現它比普通非同步更自然地適合。Rx 和資料流程很好地與非同步代碼進行交互操作。

非同步當然是代碼的相當大量,只是代碼的並不是代碼的所有的它的最佳方法。對於這篇文章的其餘部分,我會假設你已經考慮了任務並行庫和 Rx/資料流程和就結束了,等待非同步/是最適當的方法。

變換同步非同步代碼

那裡是將現有的同步代碼轉換為非同步代碼的正常程式。它是相當簡單的。一旦你完成了它幾次,它甚至可能成為相當乏味。在撰寫本文時,尚不支援自動同步-到-­非同步轉換。然而,預計將于未來幾年提出這種代碼轉換。

此過程最適合當你開始在較低級別層和你的工作方式向使用者級別。換句話說,開始引入非同步訪問資料庫或 Web Api 的資料層方法。然後介紹了在您的服務方法,然後業務邏輯和,最後,使用者層非同步。如果您的代碼不具有明確定義的圖層,你仍然可以轉換為等待非同步 /。它只會更加困難一點。

第一步是確定低級的自然非同步作業來轉換。O 根據什麼是非同步主要候選人。常見的例子是資料庫查詢和命令、 Web API 呼叫和檔案系統存取權限。很多時候,這個低級的行動已經有了現有的非同步 API。

如果底層的庫已準備非同步 API,所有你需要做是添加非同步尾碼 (或 TaskAsync 尾碼) 的同步方法名稱。例如,Entity Framework調用首先可以替換對 FirstAsync 的調用。在某些情況下,您可能想要使用一種替代類型。例如,HttpClient 是 WebClient 和區域性更多非同步友好替代。在某些情況下,您可能需要升級您的庫的版本。Entity Framework,例如,獲取非同步 API 在版本 6 中。

考慮中的代碼圖 1。這是一個簡單的例子,一個服務層和一些業務邏輯。在此示例中,都只有一個低級操作 — — 從一個在 WebDataService.Get 的 Web API 檢索 frob 識別碼字串。 這是邏輯的地方開始非同步轉換。在這種情況下,開發人員可以選擇 WebClient.DownloadString 替換 WebClient.DownloadStringTaskAsync,或用更多的非同步友好 HttpClient 替換 WebClient。

第二步是改變異步 API 呼叫,同步 API 呼叫,然後等待返回的任務。當代碼調用非同步方法時,是一般適當等待返回的任務。在這一點上,編譯器會報錯。下面的代碼將導致編譯器錯誤訊息,"'等待' 運算子可以只使用非同步方法內。考慮標記這帶有修飾符的方法 '非同步' 和其返回類型更改為任務 < 字串 >":

public sealed class WebDataService : IDataService
{
  public string Get(int id)
  {
    using (var client = new WebClient())
      return await client.DownloadStringTaskAsync(
      "http://www.example.com/api/values/" + id);
  }
}

編譯器將指導您下一步。Mark作為非同步和變化的返回類型的方法。如果同步方法的返回類型為 void,然後非同步方法的返回類型應任務。否則,任何同步方法的返回類型的 T,非同步方法的返回類型應該是任務 < T >。當您向任務 < T > 更改返回類型時,你也應該修改要結束非同步、 基於任務的非同步模式遵循的方法名稱。下面的代碼演示為非同步方法的結果的方法:

public sealed class WebDataService : IDataService
{
  public async Task<string> GetAsync(int id)
  {
    using (var client = new WebClient())
      return await client.DownloadStringTaskAsync(
      "http://www.example.com/api/values/" + id);
  }
}

之前,檢查此方法對於任何其他阻塞或同步 API 呼叫,您可以非同步休息。非同步方法不應該阻止,所以此方法應調用非同步 Api,如果它們可用。在這個簡單的例子,沒有其他阻塞的調用。在實際代碼中,留心注意重試邏輯和樂觀的衝突解決。

Entity Framework應該得到特別的提及。一個微妙的"疑難雜症"是延遲載入的相關實體。這始終是同步的。如果可能,使用額外的顯式非同步查詢,而不是延遲載入。

現在終於完成了這種方法。接下來,移到所有方法引用這一個,並再次執行此步驟。在這種情況下,WebDataService.Get 是介面實現的一部分,因此您必須更改要啟用非同步實現的介面:

public interface IDataService
{
  Task<string> GetAsync(int id);
}

接下來,移到調用方法並按照相同的步驟。你應該會中的代碼類似圖 2。不幸的是,直到所有的調用方法轉換到非同步,然後所有其調用的方法轉換為非同步,等等,就不會編譯代碼。這種級聯特性是非同步棕地開發的負擔方面。

圖 2 將所有調用方法都更改為非同步

public interface IDataService
{
  string Get(int id);
}
public sealed class WebDataService : IDataService
{
  public string Get(int id)
  {
    using (var client = new WebClient())
      return client.DownloadString("http://www.example.com/api/values/" + id);
  }
}
public sealed class BusinessLogic
{
  private readonly IDataService _dataService;
  public BusinessLogic(IDataService dataService)
  {
    _dataService = dataService;
  }
  public string GetFrob()
  {
    // Try to get the new frob id.
    var result = _dataService.Get(17);
    if (result != string.Empty)
      return result;
    // If the new one isn't defined, get the old one.
    return _dataService.Get(13);
  }
}

最終,您的代碼庫中的非同步作業的水準將增大,直到它擊中並不由你代碼中的任何其他方法調用的方法。你頂級的方法稱為直接由您正在使用哪一個架構。一些如ASP.NETMVC 框架允許非同步代碼直接。例如,ASP.NETMVC 控制器操作可以返回任務或任務 < T >。Windows Presentation Foundation(WPF) 等其他框架允許非同步事件處理常式。因此,例如,一個按鈕的 click 事件可能是非同步 void。

撞到牆上

隨著非同步代碼的級別生長在您的應用程式,你可能會達到一個點在哪裡,它似乎是不可能繼續。最常見的例子是物件導向的構造,不網與非同步代碼的功能性質。建構函式、 事件和屬性有他們自己的挑戰。

反思設計通常是圍繞這些困難的最好方式。一個常見的例子是建構函式。在同步的代碼中,建構函式方法可能會阻塞 i/o。 在非同步世界中,一個解決方案是使用非同步工廠方法,而不是建構函式。另一個例子是屬性。如果一個屬性同步阻塞 i/o,該屬性可能應該有方法。一個非同步轉換練習是非常擅長揭露這種隨著時間的推移悄悄溜進你的代碼庫的設計問題。

轉型小貼士

執行非同步代碼轉換可能會引起恐慌開始幾次,但它真的以後有點實踐成為第二天性。當你感到更舒服與非同步同步代碼轉換,在這裡您可以開始轉換過程中使用的一些技巧。

當您轉換您的代碼,留心注意併發的機會。非同步並行代碼往往是更短和比同步併發代碼簡單得多。例如,請考慮具有從 REST API 下載兩個不同資源的方法。該方法的同步版本幾乎可以肯定會下載一個,然後其他。然而,非同步版本可以輕鬆地啟動兩個下載,然後非同步等待既要完成使用 Task.WhenAll。

另一個考慮是取消。通常情況下,同步應用程式使用者習慣于等待。如果 UI 是回應在新版本中,他們可能會取消該操作的能力。非同步代碼一般應支援取消,除非有一些其他原因,它不能。 大多數情況下,非同步代碼可以只通過採用 CancellationToken 參數並將它通過傳遞給它調用的非同步方法支援取消。

您可以轉換任何代碼中使用執行緒或 BackgroundWorker 改為使用 Task.Run。Task.Run 是要比執行緒或 BackgroundWorker 撰寫容易得多。例如,它是更容易表達,"開始兩個背景計算,然後做這另一件事,當他們都已完成,"與現代的等待和 Task.Run,比與原始執行緒構造。

垂直分區

描述的方法為止作品大如果你惟一的開發人員為您的應用程式,並且您有沒有問題或會干擾你的非同步轉換工作的要求。這不是很現實的不過,它嗎?

如果你沒有時間來轉換你整個代碼庫,一下子是非同步你可以稍加修改,稱為垂直分區方法轉換。使用這種技術,你可以做你非同步轉換為代碼的某些部分。垂直分區是理想的如果你想要的只是"嘗試"非同步代碼。

若要創建垂直分區,請確定你想要使非同步使用者級代碼。也許這是一個將保存到資料庫中 (在那裡你想要保持 UI 的回應) 或使用頻繁的ASP.NET請求同樣這麼做 (在哪裡你想減少為該特定的請求所需的資源) 的 UI 按鈕的事件處理常式。遍歷代碼,奠定了該方法的調用樹。然後你可以開始在底層方法和變換你爬到樹上的方式。

毫無疑問,其他代碼將使用這些相同的底層方法。因為你不准備做出所有這些代碼非同步,解決方案是創建一個副本的方法。然後變換將非同步副本。這種方式,可以仍然生成解決方案在每一步。當你工作的使用者級代碼按照自己的方式時,你會有創建了一個垂直分區的非同步代碼在您的應用程式內。垂直分區基於我們的代碼示例將顯示中所示圖 3

圖 3 使用垂直分區將轉換為非同步程式碼片段

public interface IDataService
{
  string Get(int id);
  Task<string> GetAsync(int id);
}
public sealed class WebDataService : IDataService
{
  public string Get(int id)
  {
    using (var client = new WebClient())
      return client.DownloadString("http://www.example.com/api/values/" + id);
  }
  public async Task<string> GetAsync(int id)
  {
    using (var client = new WebClient())
      return await client.DownloadStringTaskAsync(
      "http://www.example.com/api/values/" + id);
  }
}
public sealed class BusinessLogic
{
  private readonly IDataService _dataService;
  public BusinessLogic(IDataService dataService)
  {
    _dataService = dataService;
  }
  public string GetFrob()
  {
    // Try to get the new frob id.
    var result = _dataService.Get(17);
    if (result != string.Empty)
      return result;
    // If the new one isn't defined, get the old one.
    return _dataService.Get(13);
  }
  public async Task<string> GetFrobAsync()
  {
    // Try to get the new frob id.
    var result = await _dataService.GetAsync(17);
    if (result != string.Empty)
      return result;
    // If the new one isn't defined, get the old one.
    return await _dataService.GetAsync(13);
  }
}

你可能已經注意到有一些代碼重複使用此解決方案。同步和非同步方法的所有邏輯都被都重複的這並不是很好。在一個完美的世界,代碼重複此垂直分區只是暫時的。重複的代碼將只存在於您的原始程式碼管理直到應用程式已完全轉換。在這一點上,您可以刪除所有舊的同步 Api。

然而,您不能這樣做在所有情況下。如果您正在開發一個庫 (哪怕是只在內部使用的),向後相容性是首要的任務。你可能會發現自己需要相當一段時間保持同步 Api。

有三種可能的對策,這種情況。首先,你可以開車通過非同步 Api。如果您的庫有非同步工作要做,它應公開非同步 Api。第二,你能接受作為必要的罪惡,為了向後相容的代碼重複。只有當你的團隊有特殊自律或向後相容性約束只是暫時的這是可以接受的解決辦法。

第三種解決方案是應用此處列出的駭客之一。雖然我真的不能推薦任何這些駭客,他們可以在緊要關頭。因為它們的運作是自然非同步每個俢現面向各地自然非同步操作,是知名的反模式,描述了更詳細的伺服器提供一個同步 API & 工具的博客發表于 bit.ly/1JDLmWD

阻止駭客攻擊

最直截了當的方法是簡單地阻斷的非同步版本。我建議用 GetAwaiter() 封堵。GetResult 而不是等待或結果。等待和結果將包內的 AggregateException,使複雜的錯誤處理的任何異常。示例服務層代碼會看起來像在所示的代碼圖 4 如果它用來阻止駭客。

圖 4 服務層代碼使用阻止駭客

public sealed class WebDataService : IDataService
{
  public string Get(int id)
  {
    return GetAsync(id).GetAwaiter().GetResult();
  }
  public async Task<string> GetAsync(int id)
  {
    // This code will not work as expected.
    using (var client = new WebClient())
      return await client.DownloadStringTaskAsync(
      "http://www.example.com/api/values/" + id);
  }
}

不幸的是,正如評論所示,該代碼不會實際工作。結果我"最佳做法在非同步程式設計"的文章,我剛才所述的常見僵局。

這是在哪裡駭客可以很棘手。正常的單元測試將通過,但如果從 UI 或ASP.NET上下文調用相同的代碼會鎖死。如果您使用阻止駭客,你應該寫單元測試,檢查這種行為。中的代碼圖 5 使用非同步­從我的 AsyncEx 圖書館,創建上下文類似于 UI 或ASP.NET上下文的上下文類型。

圖 5 使用 AsyncCoNtext 類型

[TestClass]
public class WebDataServiceUnitTests
{
  [TestMethod]
  public async Task GetAsync_RetrievesObject13()
  {
    var service = new WebDataService();
    var result = await service.GetAsync(13);
    Assert.AreEqual("frob", result);
  }
  [TestMethod]
  public void Get_RetrievesObject13()
  {
    AsyncContext.Run(() =>
    {
      var service = new WebDataService();
      var result = service.Get(13);
      Assert.AreEqual("frob", result);
    });
  }
}

不幸的是,正如評論所示,該代碼不會實際工作。結果我"最佳做法在非同步程式設計"的文章,我剛才所述的常見僵局。

這是在哪裡駭客可以很棘手。正常的單元測試將通過,但如果從 UI 或ASP.NET上下文調用相同的代碼會鎖死。如果您使用阻止駭客,你應該寫單元測試,檢查這種行為。中的代碼圖 5 使用非同步­從我的 AsyncEx 圖書館,創建上下文類似于 UI 或ASP.NET上下文的上下文類型。

圖 5 使用 AsyncCoNtext 類型

[TestClass]
public class WebDataServiceUnitTests
{
  [TestMethod]
  public async Task GetAsync_RetrievesObject13()
  {
    var service = new WebDataService();
    var result = await service.GetAsync(13);
    Assert.AreEqual("frob", result);
  }
  [TestMethod]
  public void Get_RetrievesObject13()
  {
    AsyncContext.Run(() =>
    {
      var service = new WebDataService();
      var result = service.Get(13);
      Assert.AreEqual("frob", result);
    });
  }
}

非同步單元測試通過,但同步單元測試無法完成。這是典型的鎖死問題。非同步代碼捕獲當前的上下文,並嘗試恢復,而同步的包裝器阻塞執行緒在這種情況下,阻止非同步作業的完成。

在這種情況下,我們的非同步代碼缺少 ConfigureAwait­(false)。然而,同樣的問題可以通過使用 WebClient 引起。WebClient 使用舊事件架構非同步模式 (EAP),它總是捕獲的上下文。所以即使您的代碼使用 ConfigureAwait(false),同樣將會發生鎖死從 WebClient 代碼。在這種情況下,你可以用更多的非同步友好 HttpClient 替換 WebClient 和得到這個工作在桌面上,如中所示圖 6

圖 6 使用 HttpClient 與 ConfigureAwait(false) 避免鎖死

public sealed class WebDataService : IDataService
{
  public string Get(int id)
  {
    return GetAsync(id).GetAwaiter().GetResult();
  }
  public async Task<string> GetAsync(int id)
  {
    using (var client = new HttpClient())
      return await client.GetStringAsync(
      "http://www.example.com/api/values/" + id).ConfigureAwait(false);
  }
}

阻止駭客需要你的團隊有嚴格的紀律性。他們需要確保 ConfigureAwait(false) 到處都需要用。他們還必須要求所有的依賴庫遵循同樣的紀律。在某些情況下,這不是可能的。在撰寫本文時,甚至 HttpClient 捕獲在某些平臺上的上下文。

阻止駭客的另一個缺點是它需要使用 ConfigureAwait­(false)。如果非同步代碼實際上需要繼續上捕獲的上下文,也只是不適合。如果您採用阻塞駭客,你強烈建議執行單元測試使用 AsyncCoNtext 或另一個類似的單線程上下文趕上任何潛在的鎖死。

執行緒池駭客

阻止駭客類似方法是卸載到執行緒池,然後塊生成任務的非同步工作。使用這個外掛程式的代碼會看起來像在所示的代碼圖 7

圖 7 代碼的執行緒池駭客

public sealed class WebDataService : IDataService
{
  public string Get(int id)
  {
    return Task.Run(() => GetAsync(id)).GetAwaiter().GetResult();
  }
  public async Task<string> GetAsync(int id)
  {
    using (var client = new WebClient())
      return await client.DownloadStringTaskAsync(
      "http://www.example.com/api/values/" + id);
  }
}

對 Task.Run 的調用執行緒池執行緒上執行非同步方法。在這裡,它將運行沒有上下文,從而避免鎖死。這種方法存在的問題之一就是非同步方法不能依賴于在特定上下文中執行。因此,它不能使用 UI 元素或ASP.NETHttpCoNtext.Current。

另一個更微妙"疑難雜症"是非同步方法可恢復任何執行緒池執行緒上。這不是一個對大多數代碼的問題。它可以是問題,如果方法使用每執行緒狀態或如果它隱式依賴于使用者介面上下文中提供的同步。

您可以創建一個後臺執行緒的上下文。我 AsyncEx 庫中的 AsyncCoNtext 類型將安裝一個單線程的上下文完成與"主回路"。這將強制恢復在同一線程上非同步代碼。這就避免了執行緒池駭客入侵的更微妙的"陷阱"。與執行緒池執行緒的主迴圈示例代碼將看起來像在所示的代碼圖 8

圖 8 使用主回路為執行緒池駭客

public sealed class WebDataService : IDataService
{
  public string Get(int id)
  {
    var task = Task.Run(() => AsyncContext.Run(() => GetAsync(id)));
    return task.GetAwaiter().GetResult();
  }
  public async Task<string> GetAsync(int id)
  {
    using (var client = new WebClient())
      return await client.DownloadStringTaskAsync(
      "http://www.example.com/api/values/" + id);
  }
}

當然,還有這種方法的缺點。執行緒池執行緒被阻止在 AsyncCoNtext 內,直到在非同步方法完成。這被阻塞的執行緒在那裡,以及主執行緒調用同步 API。因此,在調用的持續時間,有兩個執行緒都被阻塞。尤其是,對ASP.NET這種方法將大大減少應用程式的擴展能力。

國旗的論點駭客

這個外掛程式是一個沒有使用過呢。它由StephenToub 描述我在這篇文章他技術審查期間。它是一種偉大的方法和我最喜歡的所有這些駭客。

標誌參數駭客採用原始的方法,使私人,並添加一個標誌,用於指示是否該方法應運行的同步或非同步。它然後公開兩個公共的 Api,一個同步和其他非同步,如中所示圖 9

圖 9 標誌參數駭客公開兩個 Api

public interface IDataService
{
  string Get(int id);
  Task<string> GetAsync(int id);
}
public sealed class WebDataService : IDataService
{
  private async Task<string> GetCoreAsync(int id, bool sync)
  {
    using (var client = new WebClient())
    {
      return sync
        ? client.DownloadString("http://www.example.com/api/values/" + id)
        : await client.DownloadStringTaskAsync(
        "http://www.example.com/api/values/" + id);
    }
  }
  public string Get(int id)
  {
    return GetCoreAsync(id, sync: true).GetAwaiter().GetResult();
  }
  public Task<string> GetAsync(int id)
  {
    return GetCoreAsync(id, sync: false);
  }
}

在此示例中的 GetCoreAsync 方法有一個重要的屬性 — — 如果其 sync 參數為 true,它總是返回一個已經-­完成的任務。方法將阻塞時其標誌參數要求同步的行為。否則,它就像正常的非同步方法。

同步獲取包裝傳遞標誌參數為 true,然後檢索該運算的結果。請注意有沒有鎖死的機會因為任務已經完成。業務邏輯遵循類似的模式,如中所示圖 10

圖 10 適用于業務邏輯的標誌參數駭客

public sealed class BusinessLogic
{
  private readonly IDataService _dataService;
  public BusinessLogic(IDataService dataService)
  {
    _dataService = dataService;
  }
  private async Task<string> GetFrobCoreAsync(bool sync)
  {
    // Try to get the new frob id.
    var result = sync
      ? _dataService.Get(17)
      : await _dataService.GetAsync(17);
    if (result != string.Empty)
      return result;
    // If the new one isn't defined, get the old one.
    return sync
      ? _dataService.Get(13)
      : await _dataService.GetAsync(13);
  }
  public string GetFrob()
  {
    return GetFrobCoreAsync(sync: true).GetAwaiter().GetResult();
  }
  public Task<string> GetFrobAsync()
  {
    return GetFrobCoreAsync(sync: false);
  }
}

你沒有公開從你的服務層的 CoreAsync 方法的選項。這簡化了業務邏輯。然而,標誌參數方法是更多的實現細節。您將需要進行權衡反對的暴露的實現細節,缺點更簡潔的代碼的優勢,如中所示圖 11。這個外掛程式的優點是方法的邏輯基本上保持不變。它只是調用不同的 Api 基於標誌參數的值。這偉大工程如果有同步和非同步 Api,這是通常的情況之間是一一對應的關係。

圖 11 的實施細節被暴露,但代碼是乾淨

public interface IDataService
{
  string Get(int id);
  Task<string> GetAsync(int id);
  Task<string> GetCoreAsync(int id, bool sync);
}
public sealed class BusinessLogic
{
  private readonly IDataService _dataService;
  public BusinessLogic(IDataService dataService)
  {
    _dataService = dataService;
  }
  private async Task<string> GetFrobCoreAsync(bool sync)
  {
    // Try to get the new frob id.
    var result = await _dataService.GetCoreAsync(17, sync);
    if (result != string.Empty)
      return result;
    // If the new one isn't defined, get the old one.
    return await _dataService.GetCoreAsync(13, sync);
  }
  public string GetFrob()
  {
    return GetFrobCoreAsync(sync: true).GetAwaiter().GetResult();
  }
  public Task<string> GetFrobAsync()
  {
    return GetFrobCoreAsync(sync: false);
  }
}

如果您想要添加併發到非同步代碼路徑,或者如果不是理想的相應非同步 API,它不可能工作以及。例如,我寧願使用 HttpClient WebClient 在 WebDataService,但我將不得不權衡的是,它會在 GetCoreAsync 方法中增加了複雜性。

這個外掛程式的主要缺點是標誌參數是知名的反模式。Boolean 標誌,參數是一個很好的指標的方法真的是兩種不同方法之一。然而,反模式是最小化內單個類的實現細節 (除非您選擇公開你的 CoreAsync 方法)。儘管如此,它仍然是我最喜歡的駭客。

嵌套的消息迴圈駭客

這最後的駭客是我最不喜歡的。這個想法是你設置了一個嵌套的消息迴圈在 UI 執行緒中,並執行該迴圈內的非同步代碼。這種方法不是上ASP.NET的一個選項。 為各種 UI 平臺,它可能還需要不同的代碼。例如,WPF 應用程式可以使用嵌套的調度程式框架,而 Windows 表單應用程式可以在一個迴圈內使用 DoEvents。如果非同步方法不依賴于特定的 UI 平臺,你可以也使用 AsyncCoNtext 執行嵌套的迴圈,如中所示圖 12

圖 12 執行嵌套的消息與 AsyncCoNtext

public sealed class WebDataService : IDataService
{
  public string Get(int id)
  {
    return AsyncContext.Run(() => GetAsync(id));
  }
  public async Task<string> GetAsync(int id)
  {
    using (var client = new WebClient())
      return await client.DownloadStringTaskAsync(
      "http://www.example.com/api/values/" + id);
  }
}

Don不被此示例代碼的簡單所欺騙。這個外掛程式是最危險的他們所有人,因為你必須考慮可重入性。特別是該代碼使用嵌套的調度員幀或 DoEvents。在這種情況下,整個 UI 層現在必須處理意外可重入性。重入安全應用程式需要大量的周詳的考慮和規劃。

總結

在一個理想的世界中,您可以執行相對簡單的代碼轉換從同步與非同步,一切將會彩虹和獨角獸。在現實世界中,它是經常同步和非同步代碼共存的必要條件。如果你只是想試試非同步,創建垂直分區 (包含代碼重複) 直到你舒適使用非同步。如果你必須保持同步代碼為向後相容性的原因,你得忍受代碼重複或應用駭客之一。

總有一天,將只代表非同步作業,使用非同步 Api。在那之前,你必須生活在現實世界中。我希望這些技術將説明您採用非同步到您現有的應用程式,以最適合您的方式。


Stephen Cleary 是丈夫、 父親和程式師生活在密歇根北部。他曾與多執行緒和非同步程式設計 16 年和以來第一次的 CTP 在 Microsoft.NET 框架使用了非同步支援。按照他的專案和博客文章在 stephencleary.com

感謝以下的微軟技術專家對本文的審閱:James麥卡和StephenToub