async/await
是 C# 中用于编写异步代码的语法糖,它基于 Task
和 Task<T>
实现,让异步代码看起来更像同步代码,提高了可读性和可维护性。
实例讲解
我们将采用控制台应用程序进行演示。
假设我们分别使用了两种方法,即Method 1和Method 2,这两种方法不相互依赖,而Method 1需要很长时间才能完成它的任务。在同步编程中,它将执行第一个Method 1,并等待该方法的完成,然后执行Method 2。
第一个例子
在这个例子中,我们将采取两个不相互依赖的方法。
cs
class Program
{
static void Main(string[] args)
{
Method1();
Method2();
Console.ReadKey();
}
public static async Task Method1()
{
await Task.Run(() =>
{
for (int i = 0; i < 100; i++)
{
Console.WriteLine(" Method 1");
}
});
}
public static void Method2()
{
for (int i = 0; i < 25; i++)
{
Console.WriteLine(" Method 2");
}
}
}
在上面给出的代码中,Method 1和Method 2不相互依赖,我们是从主方法调用的。
在这里,我们可以清楚地看到,方法1和方法2并不是在等待对方完成。
典型输出
主线程 ID: 1
进入 Method1 - 线程 ID: 1
Task.Run 内部 - 线程 ID: 4 // 线程池线程
进入 Method2 - 线程 ID: 1 // 主线程继续执行
Method 2
Method 2
Method 1 // 两个线程的输出交错
Method 2
Method 1
...
第二个例子
如果任何第三个方法(如Method 3)都依赖于Method 1,那么它将在Wait关键字的帮助下等待Method 1的完成。我们将创建一个新的方法,作为CallMethod,在这个方法中,我们将调用我们的所有方法,分别为Method 1、Method 2和Method 3。
cs
class Program
{
static void Main(string[] args)
{
callMethod();
Console.ReadKey();
}
public static async void callMethod()
{
Task<int> task = Method1();
Method2();
int count = await task;
Method3(count);
}
public static async Task<int> Method1()
{
int count = 0;
await Task.Run(() =>
{
for (int i = 0; i < 100; i++)
{
Console.WriteLine(" Method 1");
count += 1;
}
});
return count;
}
public static void Method2()
{
for (int i = 0; i < 25; i++)
{
Console.WriteLine(" Method 2");
}
}
public static void Method3(int count)
{
Console.WriteLine("Total count is " + count);
}
}
在上面给出的代码中,Method 3需要一个参数,即Method 1的返回类型。在这里,await关键字对于等待Method 1任务的完成起着至关重要的作用。
典型输出
callMethod 线程: 1
进入 Method1 线程: 1
Task.Run 线程: 4 // 线程池线程执行循环
启动 Method2 线程: 1 // 主线程继续执行 Method2
Method 2
Method 1 // 两个线程的输出交错
Method 2
Method 1
...
等待结果的线程: 1
Method1 返回结果线程: 4
调用 Method3 线程: 1
Total count is 100
什么时候开始异步?
当被调用方法内部遇到第一个 await 时。如果方法内部没有await那么当前方法会同步执行。
假设你要开发一个桌面应用,点击按钮后下载文件并显示进度条。以下是关键代码:
cs
private async void DownloadButton_Click(object sender, EventArgs e)
{
// 1. 点击按钮,代码在 UI 线程执行
progressBar.Value = 0;
statusLabel.Text = "开始下载...";
downloadButton.Enabled = false;
try
{
// 2. 调用异步方法,注意这里有 await!
await DownloadFileAsync("https://example.com/file.zip",
new Progress<int>(percent => {
// 5. 更新进度条的代码在 UI 线程执行
progressBar.Value = percent;
statusLabel.Text = $"下载中: {percent}%";
}));
statusLabel.Text = "下载完成!";
}
catch (Exception ex)
{
statusLabel.Text = $"错误: {ex.Message}";
}
finally
{
downloadButton.Enabled = true;
}
}
private async Task DownloadFileAsync(string url, IProgress<int> progress)
{
using var client = new HttpClient();
// 3. 发起 HTTP 请求,这里是异步操作
using var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
response.EnsureSuccessStatusCode();
var totalBytes = response.Content.Headers.ContentLength;
using var contentStream = await response.Content.ReadAsStreamAsync();
// 创建本地文件流
using var fileStream = new FileStream("downloaded_file.zip", FileMode.Create);
var buffer = new byte[8192];
var bytesRead = 0;
var totalBytesRead = 0L;
// 4. 循环读取数据并写入文件,整个过程异步执行
while ((bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
await fileStream.WriteAsync(buffer, 0, bytesRead);
totalBytesRead += bytesRead;
// 计算进度并报告
if (totalBytes.HasValue && progress != null)
{
var percent = (int)(100 * totalBytesRead / totalBytes.Value);
progress.Report(percent);
}
}
}
执行流程详解
1. 初始状态
- 用户点击按钮,
DownloadButton_Click
在 UI 线程 执行。 - UI 更新(进度条归零、按钮禁用)。
2. 遇到 await
关键字
await DownloadFileAsync(...); // 关键点!
- 异步执行开始 :
DownloadFileAsync
方法被调用,但遇到第一个await
时(如client.GetAsync
):- 释放当前线程(即 UI 线程),允许 UI 继续响应(如拖动窗口、点击其他按钮)。
- 返回一个未完成的
Task
给调用者。
3. 后台执行异步操作
- 网络请求 :
HttpClient.GetAsync
在 线程池线程 中执行(但无需手动管理线程)。 - 文件写入 :
fileStream.WriteAsync
和contentStream.ReadAsync
同样在 线程池线程 中执行。
4. 进度更新如何回到 UI 线程?
progress.Report(percent); // 在下载方法中调用
- 自动上下文恢复 :
Progress<T>
的回调函数会自动在 UI 线程 执行(因为DownloadButton_Click
最初在 UI 线程启动)。 - 无需手动同步 :这是
async/await
的魔法之一!
5. 异步操作完成
-
当所有
await
操作完成后,DownloadButton_Click
从上次暂停的位置继续执行:csharp
statusLabel.Text = "下载完成!"; // 回到 UI 线程执行
流程图:线程切换过程
用户点击按钮
↓
UI 线程执行 DownloadButton_Click()
↓
调用 DownloadFileAsync()
↓
遇到第一个 await (client.GetAsync)
├─ 释放 UI 线程,允许 UI 继续响应
└─ 在后台线程执行网络请求
↓
网络请求完成
↓
继续执行 DownloadFileAsync()
↓
循环读取文件数据 (ReadAsync/WriteAsync)
└─ 每次循环都在后台线程执行,不阻塞 UI
↓
进度更新 (progress.Report)
└─ 自动在 UI 线程执行回调
↓
所有 await 完成
↓
UI 线程继续执行 DownloadButton_Click() 的剩余代码