掌握 C# 異步和等待
異步程式設計可能因其複雜的術語而令人畏懼,但它能夠顯著提升應用程式的速度和靈敏度。Tim Corey 的影片"C# 異步/等待 - 使用異步程式設計使您的應用更靈敏和快速"提供了如何有效使用這些功能的實用指南。
在本文中,我們將利用 Tim Corey 的詳細影片來拆解這些概念,透過清晰的例子展示異步與等待的實際應用。 我們將涵蓋各種場景,從異步事件處理程序到處理多個任務、同步化上下文、錯誤處理,以及像使用 async void 方法這樣的常見陷阱。 在本文結束時,您將對異步程式設計有堅實的理解,並知道如何在您的 C# 應用程式中實施它。
介紹
C# 中的異步程式設計對於提高應用程式性能至關重要,尤其是在處理 I/O 綁定或 CPU 綁定的操作時。 async 和 await 關鍵字允許開發者撰寫可執行長時間任務而不阻塞主線程的異步代碼。 透過用 async 關鍵字標記方法,您表示該方法將執行異步操作,返回一個 Task 或 Task<t>。 await 關鍵字在異步方法中使用,用於暫停執行直到等待的任務完成,允許其他操作同時執行。
不同於同步程式設計,任務順序執行並可能阻塞調用線程,異步方法使您能同時處理多個任務,例如背景進程或處理使用者輸入,而不會凍結 UI 線程。 在本文中,我們將探討 async 和 await 如何協同工作,涵蓋如 async void 方法、錯誤處理、上下文切換和同步化上下文等概念。 您將學習如何有效管理多個異步操作並處理異常,確保在調用線程、UI 線程和任務完成之間的平穩執行。 這種方法對於涉及文件系統、網路請求或並發請求的場景尤其有用,其中響應能力是關鍵。
Tim 透過解釋同步和異步操作的區別來介紹異步程序設計。 在同步程序設計中,任務按順序執行,如果某個任務花費較長時間則會導致延遲。異步程序設計允許任務並行或在背景中執行,提高了性能和響應速度。
異步程式設計的好處
Tim 強調了異步程式設計的兩大優勢:
-
改善用戶界面(UI)響應性:通過異步執行任務,即使運行長時間操作,UI 也能保持響應。
- 並行執行:獨立任務可以並行執行,縮短完成所需的總時間。
演示應用程序走過
Tim 設置了一個示範Windows Presentation Foundation (WPF)應用程式來展示同步和異步操作的區別。 該應用程序有兩個按鈕:一個用於同步執行任務,另一個用於異步執行任務。

應用程序還包含一個結果窗格以顯示輸出。
同步操作
Tim 解釋了同步操作背後的代碼。 一個 Stopwatch 用於測量任務的執行時間。
private void ExecuteSync_Click(object sender, RoutedEventArgs e)
{
Stopwatch stopwatch = Stopwatch.StartNew();
List<string> websites = GetWebsiteList();
foreach (var website in websites)
{
string result = DownloadWebsite(website);
ReportWebsiteInfo(website, result);
}
stopwatch.Stop();
ResultsWindow.Text += $"Total execution time: {stopwatch.ElapsedMilliseconds} ms{Environment.NewLine}";
}
private List<string> GetWebsiteList()
{
return new List<string>
{
"https://www.yahoo.com",
"https://www.google.com",
"https://www.microsoft.com",
"https://www.cnn.com",
"https://www.codeproject.com",
"https://www.stackoverflow.com"
};
}
private string DownloadWebsite(string websiteURL)
{
WebClient client = new WebClient();
return client.DownloadString(websiteURL);
}
private void ReportWebsiteInfo(string website, string data)
{
ResultsWindow.Text += $"{website}: {data.Length} characters{Environment.NewLine}";
}
private void ExecuteSync_Click(object sender, RoutedEventArgs e)
{
Stopwatch stopwatch = Stopwatch.StartNew();
List<string> websites = GetWebsiteList();
foreach (var website in websites)
{
string result = DownloadWebsite(website);
ReportWebsiteInfo(website, result);
}
stopwatch.Stop();
ResultsWindow.Text += $"Total execution time: {stopwatch.ElapsedMilliseconds} ms{Environment.NewLine}";
}
private List<string> GetWebsiteList()
{
return new List<string>
{
"https://www.yahoo.com",
"https://www.google.com",
"https://www.microsoft.com",
"https://www.cnn.com",
"https://www.codeproject.com",
"https://www.stackoverflow.com"
};
}
private string DownloadWebsite(string websiteURL)
{
WebClient client = new WebClient();
return client.DownloadString(websiteURL);
}
private void ReportWebsiteInfo(string website, string data)
{
ResultsWindow.Text += $"{website}: {data.Length} characters{Environment.NewLine}";
}
此代碼執行以下步驟:
- 啟動秒表:測量執行時間。
- 獲取網站列表:檢索網站 URL 列表。
- 下載每個網站:下載每個網站的內容。
- 報告網站資訊:顯示 URL 和下載內容的長度。
- 停止秒表:停止計時器並報告總執行時間。
觀察同步操作
Tim 運行應用程式以展示同步操作。 他指出,在網站下載過程中,UI 變得無反應,並且所有結果在下載完成後一次性顯示。

為了實用展示表單未移動,請在影片9:20處觀看以獲取更佳理解。
創建異步任務
Tim 透過將同步方法轉換為異步方法來解決第一個問題。 這涉及到使用 async 和 await 關鍵字。 以下是該過程的逐步說明:
-
複製現有同步方法:
- Tim 複製現有同步方法並將其重命名,以標明它將是異步的。
private async Task RunDownloadAsync() { // Same code as RunDownloadSync, but will be modified for async }private async Task RunDownloadAsync() { // Same code as RunDownloadSync, but will be modified for async } -
修改下載調用以進行異步執行:
- Tim 把下載調用包裹在
Task.Run中,以進行異步執行。
private async Task<WebsiteDataModel> DownloadWebsiteAsync(string websiteURL) { return await Task.Run(() => DownloadWebsite(websiteURL)); }private async Task<WebsiteDataModel> DownloadWebsiteAsync(string websiteURL) { return await Task.Run(() => DownloadWebsite(websiteURL)); }await關鍵字確保方法在繼續前等待異步任務完成。
- Tim 把下載調用包裹在
-
確保方法為異步的:
- 方法簽名更新為包含
async關鍵字,並返回一個 Task。
private async Task RunDownloadAsync() { List<string> websites = GetWebsiteList(); foreach (var website in websites) { var result = await DownloadWebsiteAsync(website); ReportWebsiteInfo(website, result); } }private async Task RunDownloadAsync() { List<string> websites = GetWebsiteList(); foreach (var website in websites) { var result = await DownloadWebsiteAsync(website); ReportWebsiteInfo(website, result); } } - 方法簽名更新為包含
-
正確處理事件:
- 按鈕單擊事件處理程序更新為調用新的異步方法。
private async void ExecuteAsync_Click(object sender, RoutedEventArgs e) { Stopwatch stopwatch = Stopwatch.StartNew(); await RunDownloadAsync(); stopwatch.Stop(); ResultsWindow.Text += $"Total execution time: {stopwatch.ElapsedMilliseconds} ms{Environment.NewLine}"; }private async void ExecuteAsync_Click(object sender, RoutedEventArgs e) { Stopwatch stopwatch = Stopwatch.StartNew(); await RunDownloadAsync(); stopwatch.Stop(); ResultsWindow.Text += $"Total execution time: {stopwatch.ElapsedMilliseconds} ms{Environment.NewLine}"; }- 請注意,即使事件處理程序調用異步方法,也可以返回 void。
解決UI響應性
Tim 顯示使用 await 關鍵字,UI 仍然保持響應。這允許用戶在異步任務運行時與窗口交互。
// UI remains responsive
private async void ExecuteAsync_Click(object sender, RoutedEventArgs e)
{
Stopwatch stopwatch = Stopwatch.StartNew();
await RunDownloadAsync();
stopwatch.Stop();
ResultsWindow.Text += $"Total execution time: {stopwatch.ElapsedMilliseconds} ms{Environment.NewLine}";
}
// UI remains responsive
private async void ExecuteAsync_Click(object sender, RoutedEventArgs e)
{
Stopwatch stopwatch = Stopwatch.StartNew();
await RunDownloadAsync();
stopwatch.Stop();
ResultsWindow.Text += $"Total execution time: {stopwatch.ElapsedMilliseconds} ms{Environment.NewLine}";
}
確保正確的時間安排
為了確保總執行時間的正確報告,Tim 在按鈕點擊事件處理程序的調用中添加 await 關鍵字。
// Correctly waits for the asynchronous task to complete
private async void ExecuteAsync_Click(object sender, RoutedEventArgs e)
{
Stopwatch stopwatch = Stopwatch.StartNew();
await RunDownloadAsync();
stopwatch.Stop();
ResultsWindow.Text += $"Total execution time: {stopwatch.ElapsedMilliseconds} ms{Environment.NewLine}";
}
// Correctly waits for the asynchronous task to complete
private async void ExecuteAsync_Click(object sender, RoutedEventArgs e)
{
Stopwatch stopwatch = Stopwatch.StartNew();
await RunDownloadAsync();
stopwatch.Stop();
ResultsWindow.Text += $"Total execution time: {stopwatch.ElapsedMilliseconds} ms{Environment.NewLine}";
}
通過等待異步方法的完成,準確測量執行時間,並在檢索到獲取的結果時顯示。
創建並行異步
Tim 通過使用並行執行來解決按順序完成每一個任務的限制。 以下是他修改代碼以實現此目的的方法:
-
複製現有異步方法:
- Tim 複製現有異步方法並重命名以指示並行執行。
private async Task RunDownloadParallelAsync() { // Parallel execution logic will be added here }private async Task RunDownloadParallelAsync() { // Parallel execution logic will be added here } -
創建任務清單:
- 創建一個任務列表來存儲所有下載任務。
List<Task<WebsiteDataModel>> tasks = new List<Task<WebsiteDataModel>>();List<Task<WebsiteDataModel>> tasks = new List<Task<WebsiteDataModel>>(); -
啟動所有任務而不等待:
- Tim 將它們添加到任務列表中,而不是立即等待每個下載任務。
foreach (var website in websites) { tasks.Add(DownloadWebsiteAsync(website)); }foreach (var website in websites) { tasks.Add(DownloadWebsiteAsync(website)); } -
等待所有任務完成:
- 使用
Task.WhenAll方法以等待所有任務的完成。 該方法返回一個結果陣列,一旦所有任務完成。
WebsiteDataModel[] results = await Task.WhenAll(tasks);WebsiteDataModel[] results = await Task.WhenAll(tasks); - 使用
-
處理結果:
- 在所有任務完成後,Tim 在循環中處理這些結果。
foreach (var result in results) { ReportWebsiteInfo(result); }foreach (var result in results) { ReportWebsiteInfo(result); }
這是並行執行方法的完整代碼:
private async Task RunDownloadParallelAsync()
{
List<string> websites = GetWebsiteList();
List<Task<WebsiteDataModel>> tasks = new List<Task<WebsiteDataModel>>();
foreach (var website in websites)
{
tasks.Add(DownloadWebsiteAsync(website));
}
WebsiteDataModel[] results = await Task.WhenAll(tasks);
foreach (var result in results)
{
ReportWebsiteInfo(result);
}
}
private async Task RunDownloadParallelAsync()
{
List<string> websites = GetWebsiteList();
List<Task<WebsiteDataModel>> tasks = new List<Task<WebsiteDataModel>>();
foreach (var website in websites)
{
tasks.Add(DownloadWebsiteAsync(website));
}
WebsiteDataModel[] results = await Task.WhenAll(tasks);
foreach (var result in results)
{
ReportWebsiteInfo(result);
}
}
更新事件處理程序
Tim 更新按鈕點擊事件處理程序以調用新的並行執行方法。
private async void ExecuteAsync_Click(object sender, RoutedEventArgs e)
{
Stopwatch stopwatch = Stopwatch.StartNew();
await RunDownloadParallelAsync();
stopwatch.Stop();
ResultsWindow.Text += $"Total execution time: {stopwatch.ElapsedMilliseconds} ms{Environment.NewLine}";
}
private async void ExecuteAsync_Click(object sender, RoutedEventArgs e)
{
Stopwatch stopwatch = Stopwatch.StartNew();
await RunDownloadParallelAsync();
stopwatch.Stop();
ResultsWindow.Text += $"Total execution time: {stopwatch.ElapsedMilliseconds} ms{Environment.NewLine}";
}
通過等待 RunDownloadParallelAsync,總執行時間被準確測量並報告。
觀察性能改進
Tim 透過運行並行執行的應用程序展示性能改進。 結果顯示,與順序執行相比,總執行時間大幅縮短。
// Parallel execution
async Task RunDownloadParallelAsync();
// Parallel execution
async Task RunDownloadParallelAsync();
速度提升明顯,因為網站同步下載,將總等待時間減少至最慢個別下載的持續時間。
封裝方法在Task.Run()對比異步方法調用
Tim 解釋了當您無法修改原方法時,使用 Task.Run() 來封裝同步方法並使其異步化的概念。 然而,他也展示了在可控情況下將方法本身改成異步的首選方法。
-
封裝方法在
Task.Run()中:- 當您不能更改原方法的代碼,但仍想異步執行時,此方法很有用。
private async Task<WebsiteDataModel> DownloadWebsiteAsync(string websiteURL) { return await Task.Run(() => DownloadWebsite(websiteURL)); }private async Task<WebsiteDataModel> DownloadWebsiteAsync(string websiteURL) { return await Task.Run(() => DownloadWebsite(websiteURL)); } -
使方法成為異步:
-
如果您可以修改方法,最好透過使用合適的異步API將其轉換為異步操作。
- Tim 通過將
DownloadWebsite改為DownloadWebsiteAsync並從DownloadStringTaskAsync使用DownloadStringTaskAsync來演示這一點。
private async Task<WebsiteDataModel> DownloadWebsiteAsync(string websiteURL) { WebClient client = new WebClient(); string data = await client.DownloadStringTaskAsync(websiteURL); return new WebsiteDataModel { URL = websiteURL, Data = data }; }private async Task<WebsiteDataModel> DownloadWebsiteAsync(string websiteURL) { WebClient client = new WebClient(); string data = await client.DownloadStringTaskAsync(websiteURL); return new WebsiteDataModel { URL = websiteURL, Data = data }; } -
-
調整調用方法:
- 修改方法後,調用代碼需要調整以去除
Task.Run()包裝並直接調用異步方法。
private async Task RunDownloadParallelAsync() { List<string> websites = GetWebsiteList(); List<Task<WebsiteDataModel>> tasks = new List<Task<WebsiteDataModel>>(); foreach (var website in websites) { tasks.Add(DownloadWebsiteAsync(website)); } WebsiteDataModel[] results = await Task.WhenAll(tasks); foreach (var result in results) { ReportWebsiteInfo(result); } }private async Task RunDownloadParallelAsync() { List<string> websites = GetWebsiteList(); List<Task<WebsiteDataModel>> tasks = new List<Task<WebsiteDataModel>>(); foreach (var website in websites) { tasks.Add(DownloadWebsiteAsync(website)); } WebsiteDataModel[] results = await Task.WhenAll(tasks); foreach (var result in results) { ReportWebsiteInfo(result); } } - 修改方法後,調用代碼需要調整以去除
關鍵點總結
Tim 在幾個重點速記中概述了使用 async 和 await 的重要要點:
-
確保方法返回任務:每當您將方法標記為
async時,它應該返回一個Task或Task<t>而不是 void(事件處理程序除外)。 -
為可靠的操作使用
await:當您需要在繼續操作之前獲取異步操作的結果時,使用await關鍵字。 -
為不可修改代碼包裝任務:使用
Task.Run()包裝同步方法當您不能更改原始方法時。 -
適當標記異步方法:始終在方法名後附加
Async以表明它是一個異步操作。 - 並行與順序執行:根據任務之間的依賴關係決定任務是否可以並行執行或應等待彼此。
Tim 強調,在 C# 中進行異步程式設計已透過 await 和 Task 變得簡單。 如線程上下文和公寓模型等複雜性在幕後處理。
結論
Tim Corey 的 C# 的異步與等待教程是掌握異步程式設計的寶貴資源。 透過仔細解釋像任務並行性、UI 響應性以及在正確上下文中使用 async 和 await 的重要性,Tim 向開發者提供創建更快和更靈敏應用程式的工具。 他詳細的操作指南展示了如何有效地處理長時間運行的任務而不阻塞主線程,還有管理異步操作和並行執行的策略。
為獲得更深入的了解和額外的實際示例,我強烈推薦觀看 Tim Corey 的 影片。 對於 async void 方法、錯誤處理和管理同步化上下文的見解將進一步增強您實施這些概念到專案的能力。
