C#5.0新增功能01 異步編程

  若是須要 I/O 綁定(例如從網絡請求數據或訪問數據庫),則須要利用異步編程。 還可使用 CPU 綁定代碼(例如執行成本高昂的計算),對編寫異步代碼而言,這是一個不錯的方案。C# 擁有語言級別的異步編程模型,它使你能輕鬆編寫異步代碼,而無需應付回叫或符合支持異步的庫。 它遵循基於任務的異步模式 (TAP)html

異步模型的基本概述

異步編程的核心是 Task 和 Task<T> 對象,這兩個對象對異步操做建模。 它們受關鍵字 async 和 await 的支持。 在大多數狀況下模型十分簡單:web

對於 I/O 綁定代碼,當你 await 一個操做,它將返回 async 方法中的一個 Task 或 Task<T>正則表達式

對於 CPU 綁定代碼,當你 await 一個操做,它將在後臺線程經過 Task.Run 方法啓動。數據庫

await 關鍵字有這奇妙的做用。 它控制執行 await 的方法的調用方,且它最終容許 UI 具備響應性或服務具備靈活性。編程

除上方連接的 TAP 文章中介紹的 async 和 await 以外,還有其餘處理異步代碼的方法,但本文檔將在下文中重點介紹語言級別的構造。promise

I/O 綁定示例:從 Web 服務下載數據

你可能須要在按下按鈕時從 Web 服務下載某些數據,但不但願阻止 UI 線程。 只需執行以下操做便可輕鬆實現:服務器

private readonly HttpClient _httpClient = new HttpClient();

downloadButton.Clicked += async (o, e) =>
{
    // 當來自Web服務的請求發生時,此行將向UI提供控制權。
    // UI線程如今能夠自由執行其餘工做
    var stringData = await _httpClient.GetStringAsync(URL);
    DoSomethingWithData(stringData);
};

就是這麼簡單! 代碼表示目的(異步下載某些數據),而不會在與任務對象的交互中停滯。網絡

CPU 綁定示例:爲遊戲執行計算

假設你正在編寫一個移動遊戲,在該遊戲中,按下某個按鈕將會對屏幕中的許多敵人形成傷害。執行傷害計算的開銷可能極大,並且在 UI 線程中執行計算有可能使遊戲在計算執行過程當中暫停!多線程

此問題的最佳解決方法是啓動一個後臺線程,它使用 Task.Run 執行工做,並 await 其結果。 這可確保在執行工做時 UI 能流暢運行。併發

private DamageResult CalculateDamageDone()
{
    // ··· 省略的業務邏輯代碼
    //
    //執行昂貴的計算並返回該計算的結果。
}

calculateButton.Clicked += async (o, e) =>
{
    // 此行將在計算 damagedone()執行其工做時向UI提供控制權。
// UI線程如今能夠自由執行其餘工做
var damageResult = await Task.Run(() => CalculateDamageDone()); DisplayDamage(damageResult); };

就是這麼簡單! 此代碼清楚地表達了按鈕的單擊事件的目的,它無需手動管理後臺線程,而是經過非阻止性的方式來實現。

內部原理

異步操做涉及許多移動部分。 若要了解 Task 和 Task<T> 的內部原理,請參閱深刻了解異步,以獲取詳細信息。

在 C# 方面,編譯器將代碼轉換爲狀態機,它將跟蹤相似如下內容:到達 await 時暫停執行以及後臺做業完成時繼續執行。

從理論上講,這是異步的承諾模型的實現。

需瞭解的要點
  • 異步代碼可用於 I/O 綁定和 CPU 綁定代碼,但在每一個方案中有所不一樣。
  • 異步代碼使用 Task<T> 和 Task,它們是對後臺所完成的工做進行建模的構造。
  • async 關鍵字將方法轉換爲異步方法,這使你能在其正文中使用 await 關鍵字。
  • 應用 await 關鍵字後,它將掛起調用方法,並將控制權返還給調用方,直到等待的任務完成。
  • 僅容許在異步方法中使用 await
識別 CPU 綁定和 I/O 綁定工做

前兩個示例演示如何將 async 和 await 用於 I/O 綁定和 CPU 綁定工做。 肯定所需執行的操做是 I/O 綁定或 CPU 綁定是關鍵,由於這會極大影響代碼性能,並可能致使某些構造的誤用。

如下是編寫代碼前應考慮的兩個問題:

  1. 你的代碼是否會「等待」某些內容,例如數據庫中的數據?

    若是答案爲「是」,則你的工做是 I/O 綁定。

  2. 你的代碼是否要執行開銷巨大的計算?

    若是答案爲「是」,則你的工做是 CPU 綁定。

若是你的工做爲 I/O 綁定,請使用 async 和 await (而不使用 Task.Run)。 不該使用任務並行庫 。 相關緣由在深刻了解異步的文章中說明。

若是你的工做爲 CPU 綁定,而且你重視響應能力,請使用 async 和 await,並在另外一個線程上使用 Task.Run 生成工做。 若是該工做同時適用於併發和並行,則應考慮使用任務並行庫

此外,應始終對代碼的執行進行測量。 例如,你可能會遇到這樣的狀況:多線程處理時,上下文切換的開銷高於 CPU 綁定工做的開銷。 每種選擇都有折衷,應根據自身狀況選擇正確的折衷方案。

更多示例
此代碼片斷從 www.dotnetfoundation.org 主頁下載 HTML,並對 HTML 中出現字符串「.NET」的次數計數。 它使用 ASP.NET MVC 定義執行此任務的 Web 控制器方法,以便返回數字。

若是打算在生產代碼中進行 HTML 分析,則不要使用正則表達式。 改成使用分析庫。

private readonly HttpClient _httpClient = new HttpClient();

[HttpGet]
[Route("DotNetCount")]
public async Task<int> GetDotNetCountAsync()
{
    // 掛起 GetDotNetCountAsync()方法,以容許調用方(Web服務器)接受另外一個請求,而不是阻止此請求。
    var html = await _httpClient.GetStringAsync("https://dotnetfoundation.org");

    return Regex.Matches(html, @"\.NET").Count;
}

如下是爲通用 Windows 應用編寫的相同方案,當按下按鈕時,它將執行相同的任務:

private readonly HttpClient _httpClient = new HttpClient();

private async void SeeTheDotNets_Click(object sender, RoutedEventArgs e)
{
    // 在這裏捕獲任務句柄,以便稍後等待後臺任務
    var getDotNetFoundationHtmlTask = _httpClient.GetStringAsync("https://www.dotnetfoundation.org");

    // 用戶界面線程上的任何其餘工做均可以在這裏完成,例如啓用進度條。
    // 在「等待」調用以前,這一點很重要,這樣用戶就能夠在生成此方法的執行以前看到進度條。
    NetworkProgressBar.IsEnabled = true;
    NetworkProgressBar.Visibility = Visibility.Visible;

    // await 操做符掛起 SeeTheDotNets_Click 事件,將控制權返回給調用方。
    // 這使得應用程序可以響應而不阻塞UI線程。
    var html = await getDotNetFoundationHtmlTask;
    int count = Regex.Matches(html, @"\.NET").Count;

    DotNetCountLabel.Text = $"Number of .NETs on dotnetfoundation.org: {count}";

    NetworkProgressBar.IsEnabled = false;
    NetworkProgressBar.Visibility = Visibility.Collapsed;
}

等待多個任務完成

你可能發現本身處於須要並行檢索多個數據部分的狀況。 Task API 包含兩種方法(即 Task.WhenAll 和 Task.WhenAny),這些方法容許你編寫在多個後臺做業中執行非阻止等待的異步代碼。

此示例演示如何爲一組 User 捕捉 userId 數據。

public async Task<User> GetUserAsync(int userId)
{
    // ··· 省略的業務邏輯代碼
    // 給定用戶Id {userId},檢索與數據庫中條目對應的用戶對象,其中 {userId}做爲其ID
}

public static async Task<IEnumerable<User>> GetUsersAsync(IEnumerable<int> userIds)
{
    var getUserTasks = new List<Task<User>>();

    foreach (int userId in userIds)
    {
        getUserTasks.Add(GetUserAsync(userId));
    }

    return await Task.WhenAll(getUserTasks);
}

如下是使用 LINQ 進行更簡潔編寫的另外一種方法:

public async Task<User> GetUserAsync(int userId)
{
    // ··· 省略的業務邏輯代碼
    // 給定用戶Id {userId},檢索與數據庫中條目對應的用戶對象,其中 {userId}做爲其ID
}

public static async Task<User[]> GetUsersAsync(IEnumerable<int> userIds)
{
    var getUserTasks = userIds.Select(id => GetUserAsync(id));
    return await Task.WhenAll(getUserTasks);
}

儘管它的代碼較少,但在混合 LINQ 和異步代碼時須要謹慎操做。 由於 LINQ 使用延遲的執行,所以異步調用將不會像在 foreach() 循環中那樣馬上發生,除非強制所生成的序列經過對 .ToList() 或 .ToArray() 的調用循環訪問。

重要信息和建議

儘管異步編程相對簡單,但應記住一些可避免意外行爲的要點。

  • async方法需在其主體中具備await 關鍵字,不然它們將永不暫停!

這一點需牢記在心。 若是 await 未用在 async 方法的主體中,C# 編譯器將生成一個警告,但此代碼將會以相似普通方法的方式進行編譯和運行。 請注意這會致使效率低下,由於由 C# 編譯器爲異步方法生成的狀態機將不會完成任何任務。

  • 應將「Async」做爲後綴添加到所編寫的每一個異步方法名稱中。

這是 .NET 中的慣例,以便更輕鬆區分同步和異步方法。 請注意,未由代碼顯式調用的某些方法(如事件處理程序或 Web 控制器方法)並不必定適用。 因爲它們未由代碼顯式調用,所以對其顯式命名並不重要。

  • async void 應僅用於事件處理程序。

async void 是容許異步事件處理程序工做的惟一方法,由於事件不具備返回類型(所以沒法利用 Task 和 Task<T>)。 其餘任何對 async void 的使用都不遵循 TAP 模型,且可能存在必定使用難度,例如:

  • async void 方法中引起的異常沒法在該方法外部被捕獲。

  • 十分難以測試 async void 方法。

  • 若是調用方不但願 async void 方法是異步方法,則這些方法可能會產生很差的反作用。

  • 在 LINQ 表達式中使用異步 lambda 時請謹慎

LINQ 中的 Lambda 表達式使用延遲執行,這意味着代碼可能在你並不但願結束的時候中止執行。若是編寫不正確,將阻塞任務引入其中時可能很容易致使死鎖。 此外,此類異步代碼嵌套可能會對推斷代碼的執行帶來更多困難。 Async 和 LINQ 的功能都十分強大,但在結合使用二者時應儘量當心。

  • 採用非阻止方式編寫等待任務的代碼

將阻止當前線程做爲等待任務完成的方法可能致使死鎖和已阻止的上下文線程,且可能須要更復雜的錯誤處理。 下表提供了關於如何以非阻止方式處理等待任務的指南:

使用如下方式... 而不是… 若要執行此操做
await Task.Wait 或 Task.Result 檢索後臺任務的結果
await Task.WhenAny Task.WaitAny 等待任何任務完成
await Task.WhenAll Task.WaitAll 等待全部任務完成
await Task.Delay Thread.Sleep 等待一段時間
  • 編寫狀態欠缺的代碼

請勿依賴全局對象的狀態或某些方法的執行。 請僅依賴方法的返回值。 爲何?

  • 這樣更容易推斷代碼。
  • 這樣更容易測試代碼。
  • 混合異步和同步代碼更簡單。
  • 一般可徹底避免爭用條件。
  • 經過依賴返回值,協調異步代碼可變得簡單。
  • (好處)它很是適用於依賴關係注入。

建議的目標是實現代碼中完整或接近完整的引用透明度。 這麼作能得到高度可預測、可測試和可維護的基本代碼。

其餘資源

 
相關文章
相關標籤/搜索