如果需要 I/O 綁定(例如從網(wǎng)絡(luò)請(qǐng)求數(shù)據(jù)或訪問數(shù)據(jù)庫(kù))鲸沮,則需要利用異步編程。
還可以使用 CPU 綁定代碼(例如執(zhí)行成本高昂的計(jì)算)弯淘,對(duì)編寫異步代碼而言榆综,這是一個(gè)不錯(cuò)的方案妙痹。
C# 擁有語(yǔ)言級(jí)別的異步編程模型,它使你能輕松編寫異步代碼鼻疮,而無(wú)需應(yīng)付回叫或符合支持異步的庫(kù)怯伊。 它遵循基于任務(wù)的異步模式 (TAP)。
異步模型的基本概述
對(duì)于 I/O 綁定代碼判沟,當(dāng)你 await
一個(gè)操作耿芹,它將返回 async
方法中的一個(gè) Task
或 Task<T>
。
對(duì)于 CPU 綁定代碼挪哄,當(dāng)你 await
一個(gè)操作猩系,它將在后臺(tái)線程通過 Task.Run
方法啟動(dòng)。
await
關(guān)鍵字是點(diǎn)睛之筆中燥,因?yàn)樗鼤和?duì)執(zhí)行 await
的方法的調(diào)用方的控制權(quán)。 這正是 UI 具有響應(yīng)性或服務(wù)具有靈活性的原因塘偎。
I/O 綁定示例:從 Web 服務(wù)下載數(shù)據(jù)
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 綁定示例:為游戲執(zhí)行計(jì)算
最佳解決方法是啟動(dòng)一個(gè)后臺(tái)線程疗涉,它使用 Task.Run 執(zhí)行工作,并 await 其結(jié)果吟秩。 這可確保在執(zhí)行工作時(shí) UI 能流暢運(yùn)行咱扣。
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);
};
內(nèi)部原理
異步操作涉及許多移動(dòng)部分。 若要了解 Task
和 Task<T>
的內(nèi)部原理涵防,請(qǐng)參閱深入了解異步闹伪。
在 C# 方面,編譯器將代碼轉(zhuǎn)換為狀態(tài)機(jī)壮池,它將跟蹤類似以下內(nèi)容:到達(dá) await
時(shí)暫停執(zhí)行以及后臺(tái)作業(yè)完成時(shí)繼續(xù)執(zhí)行偏瓤。
從理論上講,這是異步的承諾模型的實(shí)現(xiàn)椰憋。
需了解的要點(diǎn)
- 異步代碼可用于 I/O 綁定和 CPU 綁定代碼厅克,但在每個(gè)方案中有所不同。
- 異步代碼使用
Task<T>
和Task
橙依,它們是對(duì)后臺(tái)所完成的工作進(jìn)行建模的構(gòu)造证舟。 -
async
關(guān)鍵字將方法轉(zhuǎn)換為異步方法硕旗,這使你能在其正文中使用await
關(guān)鍵字。 - 應(yīng)用
await
關(guān)鍵字后女责,它將掛起調(diào)用方法漆枚,并將控制權(quán)返還給調(diào)用方,直到等待的任務(wù)完成抵知。 - 僅允許在異步方法中使用
await
墙基。
識(shí)別 CPU 綁定和 I/O 綁定工作
如果你的工作為 I/O 綁定,請(qǐng)使用 async
和 await
(而不使用 Task.Run
)辛藻。 不應(yīng)使用任務(wù)并行庫(kù)辅搬。
如果你的工作為 CPU 綁定,并且你重視響應(yīng)能力流妻,請(qǐng)使用 async
和 await
卷拘,并在另一個(gè)線程上使用 Task.Run
生成工作。 如果該工作同時(shí)適用于并發(fā)和并行氮墨,則應(yīng)考慮使用任務(wù)并行庫(kù)纺蛆。
此外,應(yīng)始終對(duì)代碼的執(zhí)行進(jìn)行測(cè)量规揪。 例如桥氏,你可能會(huì)遇到這樣的情況:多線程處理時(shí),上下文切換的開銷高于 CPU 綁定工作的開銷猛铅。 每種選擇都有折衷字支,應(yīng)根據(jù)自身情況選擇正確的折衷方案。
等待多個(gè)任務(wù)完成
你可能發(fā)現(xiàn)自己處于需要并行檢索多個(gè)數(shù)據(jù)部分的情況奸忽。 Task
API 包含兩種方法(即 Task.WhenAll
和 Task.WhenAny
)堕伪,這些方法允許你編寫在多個(gè)后臺(tái)作業(yè)中執(zhí)行非阻止等待的異步代碼。
此示例演示如何為一組 User 捕捉 userId 數(shù)據(jù)
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 進(jìn)行更簡(jiǎn)潔編寫的另一種方法:
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 和異步代碼時(shí)需要謹(jǐn)慎操作欠雌。 因?yàn)?LINQ 使用延遲的執(zhí)行,因此異步調(diào)用將不會(huì)像在 foreach()
循環(huán)中那樣立刻發(fā)生疙筹,除非強(qiáng)制所生成的序列通過對(duì) .ToList()
或 .ToArray()
的調(diào)用循環(huán)訪問富俄。
重要信息和建議
async
方法需在其主體中具有await
關(guān)鍵字,否則它們將永不暫停而咆!- 應(yīng)將
Async
作為后綴添加到所編寫的每個(gè)異步方法名稱中霍比。 -
async void
應(yīng)僅用于事件處理程序。
async void
是允許異步事件處理程序工作的唯一方法翘盖,因?yàn)槭录痪哂蟹祷仡愋停ㄒ虼藷o(wú)法利用Task
和Task<T>
)桂塞。 其他任何對(duì)async void
的使用都不遵循 TAP 模型,且可能存在一定使用難度馍驯,例如:-
async void
方法中引發(fā)的異常無(wú)法在該方法外部被捕獲阁危。 - 十分難以測(cè)試
async void
方法玛痊。 - 如果調(diào)用方不希望
async void
方法是異步方法,則這些方法可能會(huì)產(chǎn)生不好的副作用狂打。-
在 LINQ 表達(dá)式中使用異步 lambda 時(shí)請(qǐng)謹(jǐn)慎
LINQ 中的 Lambda 表達(dá)式使用延遲執(zhí)行擂煞,這意味著代碼可能在你并不希望結(jié)束的時(shí)候停止執(zhí)行。 如果編寫不正確趴乡,將阻塞任務(wù)引入其中時(shí)可能很容易導(dǎo)致死鎖对省。 此外,此類異步代碼嵌套可能會(huì)對(duì)推斷代碼的執(zhí)行帶來更多困難晾捏。 Async 和 LINQ 的功能都十分強(qiáng)大蒿涎,但在結(jié)合使用兩者時(shí)應(yīng)盡可能小心。
-
在 LINQ 表達(dá)式中使用異步 lambda 時(shí)請(qǐng)謹(jǐn)慎
-