1. 引言
在C#中,async
和await
关键字是用于实现异步编程的强大工具。它们的引入极大地简化了异步代码的编写,使得开发人员能够更容易地创建响应式和高性能的应用程序。但是,要真正理解它们的工作原理,我们需要深入探讨它们在底层到底在做什么。
2. 异步编程的基本概念
在深入async
和await
之前,我们需要理解一些基本概念:
同步执行: 代码按顺序执行,每个操作完成后才会进行下一个。
异步执行: 允许长时间运行的操作在后台进行,而不阻塞主线程。
任务(Task): 表示一个异步操作。
线程: 程序执行的最小单位。
3. Async 和 Await 的基本用法
让我们从一个简单的例子开始:
static async Task Main(string[] args)
{
var context = await GetWebContentAsync("http://www.baidu.com");
Console.WriteLine(context);
}
public static async Task<string> GetWebContentAsync(string url)
{
using (var client = new HttpClient())
{
string content = await client.GetStringAsync(url);
return content;
}
}
在这个例子中:
async
关键字标记方法为异步方法。
方法返回Task
,表示一个最终会产生string的异步操作。
await
用于等待GetStringAsync
方法完成,而不阻塞线程。
4. Async 方法的转换过程
当你使用async
关键字标记一个方法时,编译器会将其转换为一个状态机。这个过程大致如下:
创建一个实现了IAsyncStateMachine
接口的结构体。
将方法体转换为状态机的MoveNext
方法。
每个await
表达式都成为一个可能的暂停点,对应状态机中的一个状态。
async
方法如何被分解为多个步骤,每个await
表达式对应一个状态。
5. Await 的工作原理
await
关键字的主要作用是:
检查awaited任务是否已完成。
如果已完成,继续执行后续代码。
如果未完成,注册一个回调并返回控制权给调用者。
让我们通过一个例子来详细说明:
public async Task DoWorkAsync()
{
Console.WriteLine("开始工作");
await Task.Delay(1000); // 模拟耗时操作
Console.WriteLine("工作完成");
}
当执行到await Task.Delay(1000)
时:
检查Task.Delay(1000)
是否已完成。
如果未完成:
当Task.Delay(1000)
完成时:
触发注册的continuation。
恢复执行`await`之后的代码。
6. 异步方法的执行流程
让我们通过一个更复杂的例子来理解异步方法的执行流程:
static async Task Main(string[] args)
{
await MainMethodAsync();
Console.ReadKey();
}
public static async Task MainMethodAsync()
{
Console.WriteLine("1. 开始主方法");
await Method1Async();
Console.WriteLine("4. 主方法结束");
}
public static async Task Method1Async()
{
Console.WriteLine("2. 开始方法1");
await Task.Delay(1000);
Console.WriteLine("3. 方法1结束");
}
执行流程如下:
MainMethodAsync
开始执行,打印"1. 开始主方法"。
遇到await Method1Async()
,进入Method1Async
。
Method1Async
打印"2. 开始方法1"。
遇到await Task.Delay(1000)
,注册continuation并返回。
控制权回到MainMethodAsync
,但因为Method1Async
未完成,所以MainMethodAsync
也返回。
1秒后,Task.Delay
完成,触发continuation。
Method1Async
继续执行,打印"3. 方法1结束"。
Method1Async
完成,触发MainMethodAsync
的continuation。
MainMethodAsync
继续执行,打印"4. 主方法结束"。
7. 异常处理
async
/await
模式下的异常处理非常直观。你可以使用常规的try/catch块,异步方法中抛出的异常会被封装在返回的Task中,并在await
时重新抛出。
8. 避免常见陷阱
使用async
/await
时,有一些常见的陷阱需要注意:
8.1 死锁
考虑以下代码:
public async Task DeadlockDemoAsync()
{
await Task.Delay(1000).ConfigureAwait(false);
}
public void CallAsyncMethod()
{
DeadlockDemoAsync().Wait(); // 可能导致死锁
}
当在UI线程(或任何有同步上下文的线程)中调用CallAsyncMethod()
时,会发生死锁。
Wait()
方法会阻塞当前线程,等待异步操作完成。
当异步操作完成时,它默认会尝试在原始的同步上下文(通常是UI线程)上继续执行。
但是原始线程已经被Wait()
阻塞了,导致死锁。
8.2 忘记await
public async Task ForgetAwaitDemoAsync()
{
DoSomethingAsync(); // 忘记await
Console.WriteLine("完成"); // 这行可能在异步操作完成之前执行
}
始终记得在异步方法调用前使用await
。
8.3 过度使用async void
除了事件处理程序外,应避免使用async void
方法,因为它们的异常难以捕获和处理。
public async void BadAsyncVoidMethod()
{
await Task.Delay(1000);
throw new Exception("这个异常很难被捕获");
}
9. 高级模式
9.1 并行执行多个任务
使用Task.WhenAll
可以并行执行多个异步任务:
public async Task ParallelExecutionDemo()
{
var task1 = DoWorkAsync(1);
var task2 = DoWorkAsync(2);
var task3 = DoWorkAsync(3);
await Task.WhenAll(task1, task2, task3);
Console.WriteLine("所有任务完成");
}
public async Task DoWorkAsync(int id)
{
await Task.Delay(1000);
Console.WriteLine($"任务 {id} 完成");
}
9.2 带超时的异步操作
使用Task.WhenAny
和Task.Delay
可以实现带超时的异步操作:
static async Task Main(string[] args)
{
await FetchDataWithTimeoutAsync("http://www.google.com",new TimeSpan(0, 0, 3));
Console.ReadKey();
}
static async Task<string> FetchDataWithTimeoutAsync(string url, TimeSpan timeout)
{
using (var client = new HttpClient())
{
var dataTask = client.GetStringAsync(url);
var timeoutTask = Task.Delay(timeout);
var completedTask = await Task.WhenAny(dataTask, timeoutTask);
if (completedTask == timeoutTask)
{
throw new TimeoutException("操作超时");
}
return await dataTask;
}
}
10. 结论
async
和await
极大地简化了C#中的异步编程,使得编写高效、响应式的应用程序变得更加容易。通过将复杂的异步操作转换为看似同步的代码,它们提高了代码的可读性和可维护性。
该文章在 2024/12/9 18:38:49 编辑过