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