精通 C# 异步和等待
异步编程的复杂术语似乎令人生畏,但它在使应用程序更快、反应更灵敏方面具有显著优势。Tim Corey 的视频"C# Async / Await - 使用异步编程让您的应用程序响应更快、速度更快"提供了有效使用这些功能的实用指南。
在本文中,我们将使用 Tim Corey 的详细视频来分解这些概念,通过清晰的示例演示 async 和 await 的实际应用。 我们将涵盖各种场景,从异步事件处理程序到处理多个任务、同步上下文、错误处理以及使用异步 void 方法等常见陷阱。 本文结束时,您将对异步编程以及如何在自己的 C# 应用程序中实现异步编程有一个扎实的了解。
简介
C# 异步编程对于提高应用程序性能至关重要,尤其是在处理 I/O 绑定或 CPU 绑定操作时。 await关键字允许开发者编写异步代码,可以执行长时间运行的任务而不阻塞主线程。 通过使用Task<t>。 await关键字在异步方法中用于暂停执行,直到等待的任务完成,从而允许其他操作并发运行。
在同步编程中,任务按顺序运行并可能阻塞调用线程,而异步方法则不同,它可以让您同时处理多个任务,如后台进程或处理用户输入,而不会冻结用户界面线程。 在本文中,我们将探讨await如何协作,涵盖异步 void 方法、错误处理、上下文切换和同步上下文等概念。 您将学习如何有效地管理多个异步操作和处理异常,确保在调用线程、用户界面线程和任务完成之间顺利执行。 这种方法尤其适用于涉及文件系统、网络请求或并发请求等对响应速度要求较高的场景。
Tim 通过解释同步操作和异步操作之间的区别来介绍异步编程。 在同步编程中,任务是按顺序执行的,如果任务需要很长时间,就会造成延迟。异步编程允许并行或在后台执行任务,从而提高性能和响应速度。
异步编程的优点
Tim 强调了异步编程的两大优势:
1.提高用户界面 (UI) 的响应速度:通过异步执行任务,即使在执行长时间运行的操作时,用户界面也能保持响应速度。
2.并行执行:独立任务可以并行执行,从而减少完成任务所需的总时间。
应用程序演示
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}";
}这段代码执行以下步骤:
1.启动秒表:测量执行时间。 2.获取网站列表:检索网站 URL 列表。 3.下载每个网站:下载每个网站的内容。 4.报告网站信息:显示下载内容的 URL 和长度。 5.停止秒表:停止计时器并报告总执行时间。
观察同步操作
Tim 运行应用程序演示同步操作。 他指出,在下载网站时,用户界面会变得反应迟钝,下载完成后,结果会一次性显示出来。

关于表单不动的实际演示,请观看 9:20 处的视频,以便更好地理解。
创建异步任务
Tim 通过将同步方法转换为异步方法解决了第一个问题。 这涉及使用await关键字。 以下是翻译过程的分步说明:
1.复制现有的同步方法:
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 }
2.为异步执行修改下载调用:
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关键字确保该方法在继续之前等待异步任务完成。
3.确保方法是异步的:
方法签名被更新以包含
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); } }
4.正确处理事件:
更新按钮点击事件处理程序以调用新的异步方法。
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。
解决用户界面响应性问题
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 使用并行执行解决了等待每个任务按顺序完成的局限性。 下面是他如何修改代码以实现这一目标的过程:
1.复制现有的 Async 方法:
Tim 复制了现有的 async 方法,并将其重命名为并行执行。
private async Task RunDownloadParallelAsync() { // Parallel execution logic will be added here }private async Task RunDownloadParallelAsync() { // Parallel execution logic will be added here }
2.创建任务列表:
创建任务列表以存储所有下载任务。
List<Task<WebsiteDataModel>> tasks = new List<Task<WebsiteDataModel>>();List<Task<WebsiteDataModel>> tasks = new List<Task<WebsiteDataModel>>();
3.无需等待即可启动所有任务:
Tim 不会立即等待每个下载任务,而是将它们添加到任务列表中。
foreach (var website in websites) { tasks.Add(DownloadWebsiteAsync(website)); }foreach (var website in websites) { tasks.Add(DownloadWebsiteAsync(website)); }
4.等待所有任务完成:
使用
Task.WhenAll方法来等待所有任务完成。 完成所有任务后,该方法将返回一个结果数组。WebsiteDataModel[] results = await Task.WhenAll(tasks);WebsiteDataModel[] results = await Task.WhenAll(tasks);
5.处理结果:
所有任务完成后,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)); }
2.使方法异步:
- 如果可以修改方法,最好通过使用适当的异步 API 将方法本身转换为异步方法。
Tim通过将
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 }; }
3.调整调用方法:
在转换方法之后,调用代码需要调整,移除
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 的几个重要启示:
确保方法返回任务:每当您将一个方法标记为
Task<t>而不是void(事件处理程序除外)。使用
await关键字。对不可修改代码进行任务包装:在无法修改原方法时,使用
Task.Run()来包装同步方法。适当地标记异步方法:始终将
Async附加到方法名,以表明这是一个异步操作。- 并行执行与顺序执行:根据任务之间的依赖关系,决定任务是并行执行还是相互等待。
Tim强调在C#中使用Task使得异步编程变得更简单。 线程上下文和公寓模型等复杂问题将在幕后处理。
结论
Tim Corey 关于 C# 中 async 和 await 的教程为掌握异步编程提供了宝贵的资源。 通过仔细解释任务并行性、UI 响应性等概念,以及在正确的上下文中使用 async 和 await 的重要性,Tim 为开发人员提供了创建速度更快、响应性更强的应用程序的工具。 他详细地演示了如何在不阻塞主线程的情况下有效地处理长期运行的任务,以及管理异步操作和并行执行的策略。
如需更深入的理解和更多的实际案例,我强烈推荐您观看 Tim Corey 的 视频。 他对异步 void 方法、错误处理和同步上下文管理的见解将进一步提高您在项目中实施这些概念的能力。

