C#5.0的異步函數async\await

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

異步模型的基本概述

對于 I/O 綁定代碼,當你 await 一個操作,它將返回 async 方法中的一個 TaskTask<T>

對于 CPU 綁定代碼,當你 await 一個操作,它將在后臺線程通過 Task.Run 方法啟動。

await 關鍵字是點睛之筆,因為它暫停對執行 await 的方法的調用方的控制權。 這正是 UI 具有響應性或服務具有靈活性的原因。

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

private readonly HttpClient _httpClient = new HttpClient();

downloadButton.Clicked += async (o, e) =>
{
    // This line will yield control to the UI as the request
    // from the web service is happening.
    //
    // The UI thread is now free to perform other work.
    var stringData = await _httpClient.GetStringAsync(URL);
    DoSomethingWithData(stringData);
};

CPU 綁定示例:為游戲執行計算
最佳解決方法是啟動一個后臺線程,它使用 Task.Run 執行工作,并 await 其結果。 這可確保在執行工作時 UI 能流暢運行。

private DamageResult CalculateDamageDone()
{
    // Code omitted:
    //
    // Does an expensive calculation and returns
    // the result of that calculation.
}


calculateButton.Clicked += async (o, e) =>
{
    // This line will yield control to the UI CalculateDamageDone()
    // performs its work.  The UI thread is free to perform other work.
    var damageResult = await Task.Run(() => CalculateDamageDone());
    DisplayDamage(damageResult);
};

內部原理

異步操作涉及許多移動部分。 若要了解 TaskTask<T> 的內部原理,請參閱深入了解異步

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

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

需了解的要點

  • 異步代碼可用于 I/O 綁定和 CPU 綁定代碼,但在每個方案中有所不同。
  • 異步代碼使用 Task<T>Task,它們是對后臺所完成的工作進行建模的構造。
  • async 關鍵字將方法轉換為異步方法,這使你能在其正文中使用 await 關鍵字。
  • 應用 await 關鍵字后,它將掛起調用方法,并將控制權返還給調用方,直到等待的任務完成。
  • 僅允許在異步方法中使用 await

識別 CPU 綁定和 I/O 綁定工作

如果你的工作為 I/O 綁定,請使用 asyncawait(而不使用 Task.Run)。 不應使用任務并行庫。

如果你的工作為 CPU 綁定,并且你重視響應能力,請使用 asyncawait,并在另一個線程上使用 Task.Run 生成工作。 如果該工作同時適用于并發和并行,則應考慮使用任務并行庫

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

等待多個任務完成

你可能發現自己處于需要并行檢索多個數據部分的情況。 Task API 包含兩種方法(即 Task.WhenAllTask.WhenAny),這些方法允許你編寫在多個后臺作業中執行非阻止等待的異步代碼。

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

public async Task<User> GetUser(int userId)
{
    // Code omitted:
    //
    // Given a user Id {userId}, retrieves a User object corresponding
    // to the entry in the database with {userId} as its Id.
}

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

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

    return await Task.WhenAll(getUserTasks);
}

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

public async Task<User> GetUser(int userId)
{
    // Code omitted:
    //
    // Given a user Id {userId}, retrieves a User object corresponding
    // to the entry in the database with {userId} as its Id.
}

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

盡管它的代碼較少,但在混合 LINQ 和異步代碼時需要謹慎操作。 因為 LINQ 使用延遲的執行,因此異步調用將不會像在 foreach() 循環中那樣立刻發生,除非強制所生成的序列通過對 .ToList().ToArray() 的調用循環訪問。

重要信息和建議

  • async 方法需在其主體中具有 await 關鍵字,否則它們將永不暫停!
  • 應將Async作為后綴添加到所編寫的每個異步方法名稱中。
  • async void 應僅用于事件處理程序。
    async void 是允許異步事件處理程序工作的唯一方法,因為事件不具有返回類型(因此無法利用 TaskTask<T>)。 其他任何對 async void 的使用都不遵循 TAP 模型,且可能存在一定使用難度,例如:
    • async void 方法中引發的異常無法在該方法外部被捕獲。
    • 十分難以測試 async void 方法。
    • 如果調用方不希望 async void方法是異步方法,則這些方法可能會產生不好的副作用。
      • 在 LINQ 表達式中使用異步 lambda 時請謹慎
        LINQ 中的 Lambda 表達式使用延遲執行,這意味著代碼可能在你并不希望結束的時候停止執行。 如果編寫不正確,將阻塞任務引入其中時可能很容易導致死鎖。 此外,此類異步代碼嵌套可能會對推斷代碼的執行帶來更多困難。 Async 和 LINQ 的功能都十分強大,但在結合使用兩者時應盡可能小心。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容