MST

星途 面试题库

面试题:C# async/await与Task生命周期及异常处理

深入探讨C# async/await与Task生命周期之间的关系,特别是在异步操作链中(多个await连续使用)Task的状态变化。同时,说明在这种复杂异步场景下,异常是如何传播和处理的,以及如何编写健壮的异常处理代码来应对各种可能的异常情况。
43.1万 热度难度
编程语言C#

知识考点

AI 面试

面试题答案

一键面试

C# async/await与Task生命周期的关系

  1. 基本概念
    • async关键字用于定义一个异步方法,该方法返回一个TaskTask<T>。当方法被调用时,方法内的代码并不会立即执行完,而是在遇到第一个await时暂停。
    • await关键字用于暂停异步方法的执行,直到其等待的Task完成。当await一个Task时,await表达式的结果是该Task的最终状态(成功完成时的结果,或异常状态时抛出的异常)。
  2. Task生命周期
    • Created:当使用new Task()创建一个Task对象,但尚未调用Start()方法时,Task处于此状态。不过,在async方法中使用await时,通常不会直接创建处于Created状态的Task,而是通过方法调用(如Task.RunHttpClient.SendAsync等)返回已经处于WaitingForActivation状态或更高级状态的Task
    • WaitingForActivationTask已创建并准备好运行,但尚未开始执行。例如,Task.Run返回的Task在被调度执行前处于此状态。
    • RunningTask正在执行。
    • WaitingForChildrenToComplete:当一个Task启动了其他子Task,并且调用了Task.WaitAll或等待子Task完成时,该Task可能会进入此状态。
    • RanToCompletionTask成功完成,并且没有抛出异常。在async方法中,如果awaitTask处于此状态,await表达式将返回Task的结果(如果是Task<T>),或者继续执行后续代码(如果是Task)。
    • FaultedTask执行过程中抛出了未处理的异常。在async方法中,await一个处于Faulted状态的Task时,异常会被重新抛出。
    • CanceledTask被取消,通常是通过CancellationToken来实现。await一个处于Canceled状态的Task时,会抛出OperationCanceledException

异步操作链中Task的状态变化

  1. 连续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 task2MethodAsync再次暂停,等待task2完成。如果task2也成功完成,方法继续执行后续代码。
    • 如果task1在执行过程中抛出异常(进入Faulted状态),await task1会立即将异常重新抛出,task2将不会被执行。同样,如果task2抛出异常,异常也会被await task2重新抛出。

异常传播和处理

  1. 异常传播
    • 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没有捕获该异常,它会继续向上传播,直到被捕获或导致应用程序崩溃。
  2. 异常处理
    • 使用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)后异常处理代码可能无法访问这些上下文相关的资源。

编写健壮的异常处理代码

  1. 明确异常类型:尽量捕获具体的异常类型,而不是通用的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}");
        }
    }
    
  2. 记录异常:在捕获异常时,记录详细的异常信息,包括异常类型、消息、堆栈跟踪等,以便于调试和分析问题。可以使用日志框架(如SerilogNLog等)来记录异常。例如:
    async Task MethodAsync()
    {
        try
        {
            var task = Task.Run(() => { throw new Exception("模拟异常"); });
            await task;
        }
        catch (Exception ex)
        {
            // 使用日志记录异常
            Log.Error($"捕获到异常: {ex.Message}\n{ex.StackTrace}");
        }
    }
    
  3. 重试机制:对于一些可能由于临时故障(如网络抖动)导致的异常,可以实现重试机制。例如,使用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();
        });
    }
    
  4. 清理资源:在异常处理代码中,确保正确清理已使用的资源。例如,如果在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}");
        }
    }