面试题答案
一键面试C# async/await与Task生命周期的关系
- 基本概念
async
关键字用于定义一个异步方法,该方法返回一个Task
或Task<T>
。当方法被调用时,方法内的代码并不会立即执行完,而是在遇到第一个await
时暂停。await
关键字用于暂停异步方法的执行,直到其等待的Task
完成。当await
一个Task
时,await
表达式的结果是该Task
的最终状态(成功完成时的结果,或异常状态时抛出的异常)。
- Task生命周期
- Created:当使用
new Task()
创建一个Task
对象,但尚未调用Start()
方法时,Task
处于此状态。不过,在async
方法中使用await
时,通常不会直接创建处于Created
状态的Task
,而是通过方法调用(如Task.Run
、HttpClient.SendAsync
等)返回已经处于WaitingForActivation
状态或更高级状态的Task
。 - WaitingForActivation:
Task
已创建并准备好运行,但尚未开始执行。例如,Task.Run
返回的Task
在被调度执行前处于此状态。 - Running:
Task
正在执行。 - WaitingForChildrenToComplete:当一个
Task
启动了其他子Task
,并且调用了Task.WaitAll
或等待子Task
完成时,该Task
可能会进入此状态。 - RanToCompletion:
Task
成功完成,并且没有抛出异常。在async
方法中,如果await
的Task
处于此状态,await
表达式将返回Task
的结果(如果是Task<T>
),或者继续执行后续代码(如果是Task
)。 - Faulted:
Task
执行过程中抛出了未处理的异常。在async
方法中,await
一个处于Faulted
状态的Task
时,异常会被重新抛出。 - Canceled:
Task
被取消,通常是通过CancellationToken
来实现。await
一个处于Canceled
状态的Task
时,会抛出OperationCanceledException
。
- Created:当使用
异步操作链中Task的状态变化
- 连续await
- 当在
async
方法中有多个连续的await
时,每个await
都会等待前一个Task
完成。例如:
async Task MethodAsync() { var task1 = Task.Run(() => { /* 一些操作 */ return 1; }); var result1 = await task1; var task2 = Task.Run(() => { /* 基于result1的操作 */ return result1 + 1; }); var result2 = await task2; }
- 在这个例子中,
task1
首先被创建并开始执行。当执行到await task1
时,MethodAsync
方法暂停,等待task1
完成。如果task1
成功完成(进入RanToCompletion
状态),result1
会被赋值,然后task2
被创建并开始执行。接着执行到await task2
,MethodAsync
再次暂停,等待task2
完成。如果task2
也成功完成,方法继续执行后续代码。 - 如果
task1
在执行过程中抛出异常(进入Faulted
状态),await task1
会立即将异常重新抛出,task2
将不会被执行。同样,如果task2
抛出异常,异常也会被await task2
重新抛出。
- 当在
异常传播和处理
- 异常传播
- 在
async
方法中,当await
一个Task
时,如果该Task
处于Faulted
状态(即执行过程中抛出了未处理的异常),await
会重新抛出该异常。这个异常会沿着调用栈向上传播,直到被捕获。例如:
async Task MethodAsync() { var task = Task.Run(() => { throw new Exception("模拟异常"); }); await task; } async Task OuterMethodAsync() { await MethodAsync(); }
- 在这个例子中,
MethodAsync
中的await task
会重新抛出Task
中的异常。这个异常会传播到OuterMethodAsync
中的await MethodAsync()
,如果OuterMethodAsync
没有捕获该异常,它会继续向上传播,直到被捕获或导致应用程序崩溃。
- 在
- 异常处理
- 使用try - catch:在
async
方法中,可以使用传统的try - catch
块来捕获异常。例如:
async Task MethodAsync() { try { var task = Task.Run(() => { throw new Exception("模拟异常"); }); await task; } catch (Exception ex) { // 处理异常 Console.WriteLine($"捕获到异常: {ex.Message}"); } }
- 全局异常处理:在ASP.NET Core应用程序中,可以使用全局异常处理中间件来捕获未处理的异常。例如:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseExceptionHandler("/Error"); // 其他配置 }
- 使用ConfigureAwait(false):在某些情况下,为了避免上下文切换带来的潜在问题,可以在
await
时使用ConfigureAwait(false)
。例如:
async Task MethodAsync() { var task = Task.Run(() => { /* 操作 */ }); await task.ConfigureAwait(false); }
- 不过,在处理异常时要注意,
ConfigureAwait(false)
并不会改变异常传播和处理的基本规则,但它可能会影响异常发生时的上下文环境。例如,如果在async
方法中使用了与特定上下文(如UI线程)相关的资源,使用ConfigureAwait(false)
后异常处理代码可能无法访问这些上下文相关的资源。
- 使用try - catch:在
编写健壮的异常处理代码
- 明确异常类型:尽量捕获具体的异常类型,而不是通用的
Exception
类型,这样可以更准确地处理不同类型的异常。例如,如果是网络请求可能抛出HttpRequestException
,可以专门捕获该异常:async Task MethodAsync() { try { var client = new HttpClient(); var response = await client.GetAsync("http://example.com"); response.EnsureSuccessStatusCode(); } catch (HttpRequestException ex) { // 处理网络相关异常 Console.WriteLine($"网络请求异常: {ex.Message}"); } }
- 记录异常:在捕获异常时,记录详细的异常信息,包括异常类型、消息、堆栈跟踪等,以便于调试和分析问题。可以使用日志框架(如
Serilog
、NLog
等)来记录异常。例如:async Task MethodAsync() { try { var task = Task.Run(() => { throw new Exception("模拟异常"); }); await task; } catch (Exception ex) { // 使用日志记录异常 Log.Error($"捕获到异常: {ex.Message}\n{ex.StackTrace}"); } }
- 重试机制:对于一些可能由于临时故障(如网络抖动)导致的异常,可以实现重试机制。例如,使用
Polly
库来实现重试:using Polly; async Task MethodAsync() { var policy = Policy.Handle<HttpRequestException>() .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))); await policy.ExecuteAsync(async () => { var client = new HttpClient(); var response = await client.GetAsync("http://example.com"); response.EnsureSuccessStatusCode(); }); }
- 清理资源:在异常处理代码中,确保正确清理已使用的资源。例如,如果在
async
方法中打开了文件或数据库连接,要在异常处理时关闭这些资源,通常可以使用using
语句来自动管理资源的释放。例如:async Task MethodAsync() { try { using (var stream = new FileStream("example.txt", FileMode.Open)) { // 读取文件操作 await stream.ReadAsync(new byte[1024], 0, 1024); } } catch (IOException ex) { // 处理文件操作异常 Console.WriteLine($"文件操作异常: {ex.Message}"); } }