如果需要 I/O 綁定(例如從網絡請求數據或訪問數據庫),則需要利用異步編程。
還可以使用 CPU 綁定代碼(例如執行成本高昂的計算),對編寫異步代碼而言,這是一個不錯的方案。
C# 擁有語言級別的異步編程模型,它使你能輕松編寫異步代碼,而無需應付回叫或符合支持異步的庫。 它遵循基于任務的異步模式 (TAP)。
異步模型的基本概述
對于 I/O 綁定代碼,當你 await
一個操作,它將返回 async
方法中的一個 Task
或 Task<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);
};
內部原理
異步操作涉及許多移動部分。 若要了解 Task
和 Task<T>
的內部原理,請參閱深入了解異步。
在 C# 方面,編譯器將代碼轉換為狀態機,它將跟蹤類似以下內容:到達 await
時暫停執行以及后臺作業完成時繼續執行。
從理論上講,這是異步的承諾模型的實現。
需了解的要點
- 異步代碼可用于 I/O 綁定和 CPU 綁定代碼,但在每個方案中有所不同。
- 異步代碼使用
Task<T>
和Task
,它們是對后臺所完成的工作進行建模的構造。 -
async
關鍵字將方法轉換為異步方法,這使你能在其正文中使用await
關鍵字。 - 應用
await
關鍵字后,它將掛起調用方法,并將控制權返還給調用方,直到等待的任務完成。 - 僅允許在異步方法中使用
await
。
識別 CPU 綁定和 I/O 綁定工作
如果你的工作為 I/O 綁定,請使用 async
和 await
(而不使用 Task.Run
)。 不應使用任務并行庫。
如果你的工作為 CPU 綁定,并且你重視響應能力,請使用 async
和 await
,并在另一個線程上使用 Task.Run
生成工作。 如果該工作同時適用于并發和并行,則應考慮使用任務并行庫。
此外,應始終對代碼的執行進行測量。 例如,你可能會遇到這樣的情況:多線程處理時,上下文切換的開銷高于 CPU 綁定工作的開銷。 每種選擇都有折衷,應根據自身情況選擇正確的折衷方案。
等待多個任務完成
你可能發現自己處于需要并行檢索多個數據部分的情況。 Task
API 包含兩種方法(即 Task.WhenAll
和 Task.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
是允許異步事件處理程序工作的唯一方法,因為事件不具有返回類型(因此無法利用Task
和Task<T>
)。 其他任何對async void
的使用都不遵循 TAP 模型,且可能存在一定使用難度,例如:-
async void
方法中引發的異常無法在該方法外部被捕獲。 - 十分難以測試
async void
方法。 - 如果調用方不希望
async void
方法是異步方法,則這些方法可能會產生不好的副作用。-
在 LINQ 表達式中使用異步 lambda 時請謹慎
LINQ 中的 Lambda 表達式使用延遲執行,這意味著代碼可能在你并不希望結束的時候停止執行。 如果編寫不正確,將阻塞任務引入其中時可能很容易導致死鎖。 此外,此類異步代碼嵌套可能會對推斷代碼的執行帶來更多困難。 Async 和 LINQ 的功能都十分強大,但在結合使用兩者時應盡可能小心。
-
在 LINQ 表達式中使用異步 lambda 時請謹慎
-