.NET 開(kāi)發(fā)者最容易踩坑的六個(gè) async/await 使用錯(cuò)誤
在 .NET 中使用 async 和 await 進(jìn)行異步編程,確實(shí)讓代碼看起來(lái)更簡(jiǎn)潔、邏輯更清晰。但正因?yàn)閷?xiě)起來(lái)太“順手”,很多開(kāi)發(fā)者(包括我自己)都曾不小心掉進(jìn)過(guò)各種“坑”里。
比如程序卡死、性能下降、異常丟失、資源耗盡……這些問(wèn)題往往不是語(yǔ)法錯(cuò)誤造成的,而是對(duì)異步機(jī)制的理解不到位。
今天我就來(lái)總結(jié)一下,**.NET 開(kāi)發(fā)者最容易犯的 6 個(gè) async/await 使用錯(cuò)誤**,并告訴你正確的做法是什么。希望你看了之后能少走彎路,寫(xiě)出真正高效又穩(wěn)定的異步代碼。
常見(jiàn)錯(cuò)誤一:用了 .Result 或 .Wait() 阻塞異步方法
錯(cuò)誤示例:
var result = GetDataAsync().Result;
或者:
GetDataAsync().Wait();
為什么有問(wèn)題?
這看似只是等一個(gè)結(jié)果,但在某些上下文環(huán)境中(比如 UI 線(xiàn)程或 ASP.NET 請(qǐng)求線(xiàn)程),這樣做會(huì)導(dǎo)致死鎖!
原因在于:
- await 默認(rèn)會(huì)嘗試回到原來(lái)的上下文線(xiàn)程去繼續(xù)執(zhí)行。
- 如果主線(xiàn)程被 .Result 或 .Wait() 阻塞了,就沒(méi)人去釋放它,導(dǎo)致死循環(huán)。
正確做法:
如果當(dāng)前方法支持異步,就把它也標(biāo)記為 async,然后用 await:
var result = await GetDataAsync();
小貼士:
在 ASP.NET Core、WPF、WinForms、Blazor Server 這類(lèi)框架中,一定要避免使用 .Result 或 .Wait(),否則很容易引發(fā)死鎖問(wèn)題。
常見(jiàn)錯(cuò)誤二:寫(xiě)了 async 方法,卻沒(méi)用 await
錯(cuò)誤示例:
public async Task DoWorkAsync()
{
Task.Delay(1000); // 啥也沒(méi)干
}
有什么問(wèn)題?
這個(gè)方法雖然加了 async,但沒(méi)有用 await,所以它其實(shí)是一個(gè)同步方法,只不過(guò)多了一個(gè)狀態(tài)機(jī)包裝而已。
更糟的是,編譯器并不會(huì)報(bào)錯(cuò),你可能還以為自己寫(xiě)了個(gè)異步方法。
正確做法:
要用 await 才能真正進(jìn)入異步流程:
public async Task DoWorkAsync()
{
await Task.Delay(1000);
}
小貼士:
每一個(gè) async 方法都應(yīng)該至少有一個(gè) await;如果沒(méi)有,那就不應(yīng)該加 async。
常見(jiàn)錯(cuò)誤三:使用 async void(除了事件處理)
錯(cuò)誤示例:
public async void SaveDataAsync()
{
await Task.Delay(500);
}
為什么危險(xiǎn)?
async void 方法就像“幽靈”一樣,你無(wú)法等待它完成,也無(wú)法捕獲它的異常。
一旦拋出異常,就會(huì)直接崩潰整個(gè)應(yīng)用程序 —— 即使你在外面寫(xiě)了 try-catch 也沒(méi)用!
正確做法:
除非是事件處理函數(shù)(比如按鈕點(diǎn)擊),否則一律返回 Task:
public async Task SaveDataAsync()
{
await Task.Delay(500);
}
這樣就可以被 await 調(diào)用,并且能正確處理異常。
小貼士:
除了事件處理器,永遠(yuǎn)不要寫(xiě) async void 方法。它就像是“裸奔”的異步方法,非常不安全。
常見(jiàn)錯(cuò)誤四:明明不需要異步,卻還加 async 錯(cuò)誤示例:
public async Task<int> GetNumberAsync()
{
return 42;
}
有什么問(wèn)題?
這個(gè)方法根本沒(méi)有做任何異步操作,但卻加了 async 關(guān)鍵字,白白引入了狀態(tài)機(jī),增加了性能開(kāi)銷(xiāo)。
這不是“為了異步而異步”,而是“為了裝樣子而異步”。
正確做法:
如果你的方法就是同步返回?cái)?shù)據(jù),那就不要用 async,直接返回已完成的 Task:
public Task<int> GetNumberAsync()
{
return Task.FromResult(42);
}
?? 小貼士:
只有當(dāng)你真的在調(diào)用 I/O、數(shù)據(jù)庫(kù)、網(wǎng)絡(luò)請(qǐng)求等異步操作時(shí),才需要用 async/await,否則就別濫用。
?? 常見(jiàn)錯(cuò)誤五:每次調(diào)用都 new HttpClient
? 錯(cuò)誤示例:
public async Task<string> GetData()
{
using var client = new HttpClient(); // 每次都新建一個(gè)
return await client.GetStringAsync("https://api.example.com/data");
}
有什么風(fēng)險(xiǎn)?
每次創(chuàng)建 HttpClient 實(shí)際上都會(huì)打開(kāi)一個(gè)新的 TCP 連接,而且關(guān)閉后不會(huì)立刻釋放端口,容易造成端口耗盡(Socket Exhaustion)。
尤其是在高并發(fā)場(chǎng)景下,這種寫(xiě)法可能會(huì)讓你的應(yīng)用突然“掛掉”。
正確做法:
把 HttpClient 當(dāng)作共享資源來(lái)使用,推薦通過(guò)依賴(lài)注入的方式獲?。?/p>
private readonly HttpClient _httpClient;
public MyService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<string> GetData()
{
return await _httpClient.GetStringAsync("https://api.example.com/data");
}
這樣不僅復(fù)用了連接,還能更好地控制生命周期。
小貼士:
HttpClient 是設(shè)計(jì)用來(lái)長(zhǎng)期使用的,頻繁 new 它是一種“反模式”。建議配合 IHttpClientFactory 或服務(wù)注入一起使用。
常見(jiàn)錯(cuò)誤六:忽略 ConfigureAwait(false),導(dǎo)致上下文捕獲引發(fā)死鎖
錯(cuò)誤示例:
// 默認(rèn)捕獲當(dāng)前上下文,可能導(dǎo)致線(xiàn)程阻塞
public async Task DoWorkAsync()
{
await SomeIoOperationAsync(); // 默認(rèn)會(huì)嘗試回到原始上下文
}
有什么問(wèn)題?
在很多異步庫(kù)或框架中(如 ASP.NET 或 WPF),await 默認(rèn)會(huì)嘗試捕獲當(dāng)前的同步上下文(Synchronization Context),并在任務(wù)完成后回到這個(gè)上下文繼續(xù)執(zhí)行后續(xù)代碼。
這在 UI 應(yīng)用中是有意義的,但在非 UI 層(如類(lèi)庫(kù)、服務(wù)層)中,這種行為反而可能帶來(lái)不必要的性能開(kāi)銷(xiāo),甚至在某些情況下引發(fā)死鎖。
正確做法:
在非 UI 代碼中,建議加上 .ConfigureAwait(false) 來(lái)避免上下文捕獲:
// 避免上下文捕獲,提高性能并防止死鎖
public async Task DoWorkAsync()
{
await SomeIoOperationAsync().ConfigureAwait(false);
}
小貼士:
在類(lèi)庫(kù)、通用方法、后臺(tái)服務(wù)中,建議始終加上 .ConfigureAwait(false),除非你確實(shí)需要回到原始上下文。
總結(jié):async/await 的最佳實(shí)踐清單
問(wèn)題 | 推薦做法 |
? 使用 | ? 改成 |
? 寫(xiě)了 | ? 該刪就刪,不該加就別加 |
? 亂用 | ? 僅限事件處理,其他一律用 |
? 不需要異步卻加了 | ? 用 |
? 每次都 new HttpClient | ? 全局復(fù)用或通過(guò) DI 獲取 |
? 忽略 | ? 非 UI 代碼中加上 |
寫(xiě)在最后
async/await 是 .NET 中非常強(qiáng)大的工具,但也是一把雙刃劍。用得好,能讓你的應(yīng)用響應(yīng)更快、吞吐更高;用不好,輕則性能下降,重則系統(tǒng)崩潰。
這篇文章列出的六個(gè)常見(jiàn)錯(cuò)誤,都是我們?cè)趯?shí)際項(xiàng)目中最容易踩到的“地雷”。希望你能從中吸取經(jīng)驗(yàn)教訓(xùn),寫(xiě)出更穩(wěn)定、更高效的異步代碼。