在 C# 中将 Async/Await 与 .NET Core 配合使用的最佳做法

作者:微信公众号:【架构师老卢】
1-30 19:14
31

概述:在现代软件开发领域,编写异步代码对于确保应用程序的响应性和可扩展性至关重要。C# 和 .NET Core 提供了强大的工具,用于使用 和 关键字管理异步操作。在这篇博客文章中,我们将探讨在 C# 中与 .NET Core 结合使用的最佳实践,从简单的示例开始,逐步转向更复杂的方案。

在现代软件开发领域,编写异步代码对于确保应用程序的响应性和可扩展性至关重要。C# 和 .NET Core 提供了强大的工具,用于使用 和 关键字管理异步操作。在这篇博客文章中,我们将探讨在 C# 中与 .NET Core 结合使用的最佳实践,从简单的示例开始,逐步转向更复杂的方案。asyncawaitasync/await

为什么要使用 Async/Await?

在深入研究最佳实践之前,让我们简要了解一下为什么我们在 C# 中使用:async/await

  1. 响应式用户界面:异步代码允许应用程序的用户界面保持响应,即使在执行耗时的操作(如数据库查询或 Web 请求)时也是如此。
  2. 提高可扩展性:异步操作使您能够有效地处理大量并发任务,从而使应用程序更具可扩展性。
  3. 资源效率:通过避免阻塞调用,异步代码允许线程有效地重用,从而减少资源消耗。
  4. 更好的用户体验:更快地执行长时间运行的任务会带来更好的整体用户体验。

简单异步/等待使用的最佳实践

1. 尽可能使用异步

每当遇到 I/O 绑定操作(如数据库查询、网络请求或文件 I/O)时,请考虑使方法 .例如:async

using System;  
using System.Net.Http;  
using System.Threading.Tasks;  
  
public class AsyncExample  
{  
    public async Task<string> FetchDataAsync()  
    {  
        // Simulate an HTTP request  
        using (var client = new HttpClient())  
        {  
            var response = await client.GetAsync("https://jsonplaceholder.typicode.com/posts/1");  
            return await response.Content.ReadAsStringAsync();  
        }  
    }  
  
    public static async Task Main(string[] args)  
    {  
        var example = new AsyncExample();  
        var result = await example.FetchDataAsync();  
        Console.WriteLine(result);  
    }  
}

解释:

  • 在此示例中,我们使用异步方法创建一个简单的类,该方法使用 模拟 HTTP 请求。AsyncExampleFetchDataAsyncHttpClient
  • 我们习惯于异步等待 HTTP 响应和内容被读取。await
  • 在该方法中,我们创建一个实例并异步调用,将结果打印到控制台。MainAsyncExampleFetchDataAsync

示例 2:使用“异步”后缀

using System;  
using System.Threading.Tasks;  
  
public class AsyncExample  
{  
    public async Task ProcessDataAsync()  
    {  
        // Simulate an async operation  
        await Task.Delay(1000); // Delay for 1 second  
        Console.WriteLine("Data processing completed.");  
    }  
  
    public static async Task Main(string[] args)  
    {  
        var example = new AsyncExample();  
        await example.ProcessDataAsync();  
    }  
}

解释:

  • 在此示例中,我们有一个类,其中包含一个名为 的异步方法。AsyncExampleProcessDataAsync
  • 该方法通过使用 模拟异步操作。Task.Delay
  • 为了清楚起见,我们遵循命名约定,并将“Async”附加到方法名称中。

示例 3:用于非 UI 线程ConfigureAwait(false)

using System;  
using System.Threading.Tasks;  
  
public class AsyncExample  
{  
    public async Task DoSomethingAsync()  
    {  
        await Task.Delay(1000).ConfigureAwait(false); // ConfigureAwait(false) for non-UI thread  
        Console.WriteLine("Operation completed.");  
    }  
  
    public static async Task Main(string[] args)  
    {  
        var example = new AsyncExample();  
        await example.DoSomethingAsync();  
    }  
}

解释:

  • 在此示例中,该方法包括 when 等待任务。这对于非 UI 线程(例如在控制台应用程序或库中)非常重要,以避免潜在的死锁。DoSomethingAsyncConfigureAwait(false)

示例 4:避免使用async void

using System;  
using System.Threading.Tasks;  
  
public class AsyncExample  
{  
    // Define an event  
    public event EventHandler<string> OperationCompleted;  
  
    public async Task DoSomethingAsync()  
    {  
        await Task.Delay(1000);  
        Console.WriteLine("Operation completed.");  
  
        // Raise the event when the operation is completed  
        OperationCompleted?.Invoke(this, "Operation completed.");  
    }  
  
    public async void HandleAsyncEvent()  
    {  
        await DoSomethingAsync();  
    }  
  
    public static async Task Main(string[] args)  
    {  
        var example = new AsyncExample();  
  
        // Subscribe to the event  
        example.OperationCompleted += (sender, message) =>  
        {  
            Console.WriteLine("Event handler: " + message);  
        };  
  
        await example.DoSomethingAsync(); // Good practice  
        example.HandleAsyncEvent(); // Only for asynchronous event handlers  
    }  
}

解释:

  1. 我们定义一个使用委托命名的事件。异步操作完成后将引发此事件,并将消息作为字符串参数进行传递。**避免用于 常规方法。这仅保留给异步事件处理程序。**OperationCompletedEventHandler<string>async void
  2. 在该方法中,我们执行具有延迟的异步操作,以模拟正在完成的工作。操作完成后,我们调用事件,传递消息“操作已完成”。DoSomethingAsyncOperationCompleted
  3. 该方法保持不变,并继续作为异步事件处理程序。HandleAsyncEvent
  4. 在该方法中,我们创建了一个 的实例。我们还使用 lambda 表达式订阅该事件。引发事件时,lambda 表达式会将收到的消息打印到控制台。MainAsyncExampleOperationCompleted

通过这个完整的事件处理程序示例,您可以了解如何与事件结合使用。当 中的异步操作完成时,它将引发事件,并且方法中的事件处理程序通过打印消息来对它做出反应。此模式对于异步通知和事件驱动编程非常有用。async/awaitDoSomethingAsyncMain

更复杂的异步方案的最佳做法

5. 编写异步方法

使用或并发或按顺序执行多个异步操作来编写多个异步操作。Task.WhenAllTask.WhenAny

using System;  
using System.Collections.Generic;  
using System.Threading.Tasks;  
  
public class AsyncExample  
{  
    public async Task<string> FetchDataAsync()  
    {  
        await Task.Delay(2000); // Simulate data fetching  
        return "Data fetched successfully.";  
    }  
  
    public async Task\<string\> ProcessDataAsync()  
    {  
        await Task.Delay(1500); // Simulate data processing  
        return "Data processed successfully.";  
    }  
  
    public async Task<string> SaveDataAsync()  
    {  
        await Task.Delay(1000); // Simulate data saving  
        return "Data saved successfully.";  
    }  
  
    public async Task ProcessDataConcurrentlyAsync()  
    {  
        var tasks = new List<Task<string>> // Use Task\<string> to store results  
        {  
            FetchDataAsync(),  
            ProcessDataAsync(),  
            SaveDataAsync()  
        };  
  
        // Start all tasks concurrently and wait for all to complete  
        string[] results = await Task.WhenAll(tasks);  
  
        // Process results or perform other operations  
        foreach (string result in results)  
        {  
            Console.WriteLine(result);  
        }  
    }  
  
    public static async Task Main(string[] args)  
    {  
        var example = new AsyncExample();  
        await example.ProcessDataConcurrentlyAsync();  
    }  
}

解释:

  1. 在此示例中,我们有三个异步方法:、 和 。每种方法都模拟具有特定延迟的操作。FetchDataAsyncProcessDataAsyncSaveDataAsync
  2. 该方法将这些异步方法组合成一个任务列表,每个任务返回一个字符串结果。我们用来存储任务及其结果。ProcessDataConcurrentlyAsyncList<Task<string>>
  3. Task.WhenAll用于并发启动所有任务,并等待所有任务完成。这使我们能够同时执行多个异步操作,从而提高整体性能。
  4. 完成所有任务后,将结果收集到字符串数组中。您可以处理这些结果或执行任何其他必要的操作。在此示例中,我们只需将每个结果打印到控制台即可。results

此代码演示了使用 组合异步操作的强大功能。它允许您同时执行多个异步任务,并有效地等待所有任务完成。这在需要并行执行多个异步操作(例如从不同源获取数据、处理数据和同时保存数据)的情况下特别有用。Task.WhenAll

6. 异常处理

使用块在异步代码中正确处理异常。避免吞下异常;相反,请记录或报告它们。try-catch

using System;  
using System.Threading.Tasks;  
  
public class AsyncExample  
{  
    public async Task<int> DivideAsync(int dividend, int divisor)  
    {  
        try  
        {  
            // Simulate a potentially problematic async operation  
            await Task.Delay(1000); // Delay for 1 second  
  
            if (divisor == 0)  
            {  
                throw new DivideByZeroException("Divisor cannot be zero.");  
            }  
  
            return dividend / divisor;  
        }  
        catch (DivideByZeroException ex)  
        {  
            // Handle specific exception  
            Console.WriteLine("DivideByZeroException: " + ex.Message);  
            throw; // Rethrow the exception  
        }  
        catch (Exception ex)  
        {  
            // Handle general exception  
            Console.WriteLine("An error occurred: " + ex.Message);  
            throw; // Rethrow the exception  
        }  
    }  
  
    public static async Task Main(string[] args)  
    {  
        var example = new AsyncExample();  
  
        try  
        {  
            int result = await example.DivideAsync(10, 2);  
            Console.WriteLine("Result: " + result);  
        }  
        catch (Exception ex)  
        {  
            // Handle exceptions here  
            Console.WriteLine("Exception caught in Main: " + ex.Message);  
        }  
    }  
}

解释:

  1. 在此示例中,我们有一个异步方法,该方法模拟延迟为 1 秒的潜在问题操作。DivideAsync
  2. 在该方法中,我们使用一个块来处理异常。有两个块:DivideAsynctry-catchcatch
  • 第一个块捕获特定的,并通过打印特定的错误消息来处理它。然后,我们使用 重新抛出异常,允许它向上传播调用堆栈。catchDivideByZeroExceptionthrow
  • 第二个块捕获一般异常,并通过打印一般错误消息来处理它们。同样,我们重新抛出异常以确保它被传播。catch

在该方法中,我们在块内使用有效参数 ( 和 ) 进行调用。我们在块中处理异常,在块中打印异常消息。MainDivideAsync102trycatch

该代码演示如何处理异步方法中的特定异常和常规异常。正确捕获和处理异常非常重要,无论这些异常是特定于您的操作还是更一般的异常。此外,重新引发异常可确保它们不会被吞噬,从而允许更高级别的代码在必要时处理它们。throw

当您运行此代码时,它将成功执行除法操作,您将看到打印到控制台的“结果”。但是,如果将除数更改为 ,它将引发一个 ,并且您将看到打印到控制台的相应异常处理消息。0DivideByZeroException

7. 取消令牌

当您需要正常取消异步方法时,请将其传递给异步方法。这对于用户发起的取消等情况非常有用。CancellationToken

using System;  
using System.Threading;  
using System.Threading.Tasks;  
  
public class AsyncExample  
{  
    public async Task DownloadFileAsync(string url, CancellationToken cancellationToken)  
    {  
        try  
        {  
            Console.WriteLine("Downloading file from " + url);  
  
            using (var client = new HttpClient())  
            {  
                // Simulate a long-running download  
                await Task.Delay(1000, cancellationToken); // Delay for 1 second, can be canceled  
  
                if (cancellationToken.IsCancellationRequested)  
                {  
                    Console.WriteLine("Download canceled.");  
                    cancellationToken.ThrowIfCancellationRequested();  
                }  
  
                Console.WriteLine("Download completed.");  
            }  
        }  
        catch (OperationCanceledException ex)  
        {  
            Console.WriteLine("OperationCanceledException: " + ex.Message);  
        }  
    }  
  
    public static async Task Main(string[] args)  
    {  
        var example = new AsyncExample();  
  
        // Create a CancellationTokenSource to manage the cancellation  
        using (var cancellationTokenSource = new CancellationTokenSource())  
        {  
            // Simulate user-initiated cancellation after 500ms  
            cancellationTokenSource.CancelAfter(500);  
  
            try  
            {  
                await example.DownloadFileAsync("https://example.com/bigfile.zip", cancellationTokenSource.Token);  
            }  
            catch (Exception ex)  
            {  
                Console.WriteLine("Exception caught: " + ex.Message);  
            }  
        }  
    }  
}

解释:

  1. 在此示例中,我们有一个异步方法,用于模拟从 URL 下载文件。我们将 a 传递给此方法以允许正常取消。DownloadFileAsyncCancellationToken
  2. 在该方法中,我们使用一个块来处理异常。我们还使用 检查是否已取消。DownloadFileAsynctry-catchCancellationTokencancellationToken.IsCancellationRequested
  3. 我们使用 模拟长时间运行的下载操作。如果发出信号,则可以取消此延迟。await Task.Delay(1000, cancellationToken)CancellationToken
  4. 如果请求取消 (),我们会打印一条消息并抛出一个 .这可确保可以正常取消操作。cancellationToken.IsCancellationRequestedOperationCanceledException
  5. 在该方法中,我们创建了一个 和 a 的实例来管理取消。MainAsyncExampleCancellationTokenSource
  6. 我们通过调用 模拟 500 毫秒后用户发起的取消。cancellationTokenSource.CancelAfter(500)
  7. 然后,我们使用提供的 URL 和来自源的 进行调用。如果操作被取消,则会抛出并捕获。DownloadFileAsyncCancellationTokenOperationCanceledException

此示例演示如何使用正常取消异步操作。它允许您实施用户启动的取消或处理需要停止正在进行的操作而不突然终止应用程序的情况。运行此代码时,您将看到相应的消息,指示下载进度和取消。CancellationToken

8. 库代码中的 ConfigureAwait(false)

在编写可重用的库代码时,请随意使用以防止使用者应用程序中出现潜在的死锁。将同步上下文的选择留给调用方。ConfigureAwait(false)

在 .NET 中,异步代码执行可以与同步上下文相关联。同步上下文确定异步操作的计划和执行方式。在大多数 UI 应用程序中,都有一个同步上下文,可确保异步代码在 UI 线程上运行以更新用户界面组件。

但是,在某些情况下,尤其是在编写库代码时,使用同步上下文可能会导致死锁或性能低下。这是因为阻塞 UI 线程或其他特殊线程可能会导致应用程序性能无响应或下降。为避免这种情况,您可以在库代码中等待异步操作时使用。ConfigureAwait(false)

using System;  
using System.Threading.Tasks;  
  
public class LibraryCode  
{  
    public async Task<string> SomeLibraryMethod()  
    {  
        // Simulate an asynchronous operation  
        await Task.Delay(1000).ConfigureAwait(false);  
  
        // Return a result  
        return "Library operation completed.";  
    }  
}

在此示例中,我们有一个名为 的库类,它包含一个名为 的方法。在此方法中:LibraryCodeSomeLibraryMethod

我们使用 执行异步操作。通过使用 ,我们显式指定不希望捕获当前同步上下文。await Task.Delay(1000).ConfigureAwait(false);ConfigureAwait(false)

现在,让我们在消费者应用程序中使用此库:

using System;  
using System.Threading.Tasks;  
  
public class ConsumerApp  
{  
    public async Task UseLibraryCode()  
    {  
        var library = new LibraryCode();  
        string result = await library.SomeLibraryMethod();  
  
        Console.WriteLine("Result: " + result);  
    }  
}

在消费者应用程序中,我们创建一个实例并异步调用。LibraryCodeSomeLibraryMethod

库代码中的解释ConfigureAwait(false)

  • 当消费者应用程序调用时,它会等待库的异步操作的结果。await library.SomeLibraryMethod();
  • 如果省略库,则库方法将捕获使用者应用程序的同步上下文(可以是 GUI 应用程序中的 UI 上下文)。ConfigureAwait(false)
  • 通过使用 ,库方法可确保它不会捕获使用者应用程序的同步上下文。这意味着,当库代码完成其异步操作时,它不会尝试返回到可能导致死锁或其他同步问题的原始同步上下文(例如,UI 线程)。ConfigureAwait(false)

总之,在库代码中使用是一种很好的做法,可以防止潜在的死锁,并使库在各种应用程序上下文中使用时更加健壮。它将同步上下文的选择权留给调用方,使使用者能够控制他们想要如何处理异步操作,并避免意外的 UI 线程阻塞或性能问题。ConfigureAwait(false)

9. 分析和优化

分析异步代码以识别性能瓶颈。通过将 CPU 密集型操作卸载到单独的线程池来优化这些操作。在此最佳实践中,我们将探讨如何将 CPU 密集型操作卸载到单独的线程池,以防止阻塞主线程。下面是一个完整的代码片段来说明这一点:

using System;  
using System.Diagnostics;  
using System.Threading.Tasks;  
  
public class AsyncExample  
{  
    public async Task HeavyCpuBoundOperationAsync()  
    {  
        var stopwatch = Stopwatch.StartNew();  
  
        // Offload a CPU-bound operation to a separate thread pool  
        await Task.Run(() =>  
        {  
            // Simulate a CPU-bound operation  
            for (int i = 0; i < 1000000; i++)  
            {  
                // Perform some heavy computation  
                Math.Sqrt(i);  
            }  
        });  
  
        stopwatch.Stop();  
        Console.WriteLine("CPU-bound operation completed in " + stopwatch.ElapsedMilliseconds + "ms");  
    }  
  
    public static async Task Main(string[] args)  
    {  
        var example = new AsyncExample();  
  
        Console.WriteLine("Starting CPU-bound operation...");  
        await example.HeavyCpuBoundOperationAsync();  
        Console.WriteLine("CPU-bound operation finished.");  
  
        // Continue with other asynchronous work or application logic  
    }  
}

解释:

  1. 在此示例中,我们有一个名为 的方法,它表示受 CPU 限制的操作。此操作是计算密集型的,如果同步执行,可能会阻塞主线程。HeavyCpuBoundOperationAsync
  2. 在内部,我们习惯于将 CPU 密集型工作卸载到单独的线程池中。这允许 CPU 绑定操作并发运行,而不会阻塞主线程。HeavyCpuBoundOperationAsyncTask.Run
  3. 我们在 CPU 绑定操作之前启动秒表,然后停止它以测量经过的时间。这有助于我们分析操作的性能。
  4. 在该方法中,我们创建一个实例并调用 .这演示了如何在异步上下文中将卸载技术用于 CPU 密集型操作。MainAsyncExampleHeavyCpuBoundOperationAsync
  5. 我们在 CPU 绑定操作之前和之后打印消息,以指示它何时开始和结束。

通过使用 将 CPU 绑定操作卸载到单独的线程池,可以防止它阻塞主线程,并使应用程序保持响应。使用秒表分析操作的执行时间有助于识别性能瓶颈。需要注意的是,并非所有操作都应卸载;您应该只对有意义的 CPU 密集型任务执行此操作。此最佳做法可确保应用程序在高效处理资源密集型任务的同时保持响应。Task.Run

结论

在 C# 中将 .NET Core 与 .NET Core 结合使用可以大大提高应用程序的响应能力和可伸缩性。通过遵循这些最佳实践,您可以编写干净、高效且可维护的异步代码。从简单的示例开始,逐步将这些做法合并到更复杂的异步方案中,以确保平稳过渡。祝您编码愉快!async/await

阅读排行