控制并发和性能的 6 个高级异步编程概念

作者:微信公众号:【架构师老卢】
11-19 19:7
68

如果你已经熟悉了基础知识——使用 asyncawait 来保持应用程序的响应性——那么现在是时候来探索.NET 中异步编程更高级的内容了。在这里,我们将更深入地探讨如何控制并发、高效管理多个任务以及处理异步编程中的常见难题。

异步编程:让你的代码响应迅速且高效

你是否曾使用过某个应用程序,它突然卡住或者让你干等着呢?挺让人懊恼的,对吧?这恰恰就是…… piyushdoorwar.medium.com

为何要深入学习异步编程的高级用法?

想象一下,你正在构建一个处理大量数据的应用程序,它要在后台处理成千上万条记录,或者你正在开发一个客户端,它需要同时调用多个 API 来汇总数据。基本的异步技术固然不错,但对于这些场景而言,高级异步编程能帮助你一次性处理多个任务,同时又不会让系统不堪重负。

1. 使用 Task.WhenAll 实现并行

当你需要启动多个相互独立的任务时,Task.WhenAll 会是你的好帮手。想象一下,你正在开发一款旅行应用程序,它需要同时查询多个预订系统(航班、酒店、租车)。这些系统中的每一个可能都需要几秒钟来响应,而你并不想依次等待每个系统的响应。

public async Task<BookingSummary> GetBookingSummaryAsync()
{
    var flightsTask = GetFlightsAsync();
    var hotelsTask = GetHotelsAsync();
    var carsTask = GetCarsAsync();

    await Task.WhenAll(flightsTask, hotelsTask, carsTask);

    return new BookingSummary
    {
        Flights = flightsTask.Result,
        Hotels = hotelsTask.Result,
        Cars = carsTask.Result
    };
}

在这里,Task.WhenAll 会同时启动所有任务,并等待它们全部完成后再继续执行后续操作。对于那些相互不依赖的操作而言,这可以显著减少等待时间。

2. 处理任务取消

在高级异步编程中,常常会涉及到一些长时间运行且有时需要被取消的任务——想象一下,用户想要中途停止文件下载的情况。在.NET 中,你可以使用 CancellationToken 来处理这种场景。

public async Task DownloadFileAsync(string url, CancellationToken cancellationToken)
{
    using (var httpClient = new HttpClient())
    {
        var response = await httpClient.GetAsync(url, cancellationToken);
        var data = await response.Content.ReadAsByteArrayAsync(cancellationToken);
        // 处理数据(例如,保存到磁盘)
    }
}

在这段代码中,将 CancellationToken 传递给异步方法后,如果用户取消操作,你就可以停止下载。将此与用户界面(UI)更新相结合,以便在操作成功取消时通知用户——这总能带来良好的用户体验!

3. 使用 SemaphoreSlim 限制并发量

在处理大规模并行任务时,你可能想要避免让应用程序或外部资源过载。SemaphoreSlim 可以限制并发任务的数量,在处理像数据库写入或 API 请求这类资源密集型任务时,它尤为有用。

想象一下有这样一个应用程序,它要向数千名用户发送通知。与其一次性发送所有通知,不如将它们分批发送,以避免使服务器不堪重负。

private static SemaphoreSlim _semaphore = new SemaphoreSlim(10); // 限制为10个并发任务

public async Task SendNotificationsAsync(List<User> users)
{
    var tasks = users.Select(async user =>
    {
        await _semaphore.WaitAsync();
        try
        {
            await SendNotificationAsync(user);
        }
        finally
        {
            _semaphore.Release();
        }
    });
    
    await Task.WhenAll(tasks);
}

在这个示例中,同时只会发送10个通知,这样便于管理,也能防止系统过载。

4. 使用 ConfigureAwait(false) 防止死锁

异步编程中一个常见的问题就是死锁,当涉及到主 UI 线程时,这种情况经常发生。在使用库中的异步方法时,添加 .ConfigureAwait(false) 会告知代码不要捕获当前上下文,这有助于避免死锁,尤其是在非 UI 代码中。

public async Task<int> CalculateAsync()
{
    await Task.Delay(1000).ConfigureAwait(false);
    return 42;
}

添加 .ConfigureAwait(false) 还可以略微提升性能,因为它跳过了上下文切换的步骤。不过,这种做法适用于库代码和后台代码,而 UI 应用程序仍然需要上下文来更新界面。

5. 使用 AsyncLocal 存储异步上下文数据

有时候,你需要在异步调用之间传递数据,比如用于跟踪日志的用户 ID 或者用于调试的事务 ID。AsyncLocal 提供了一种在异步调用之间保持数据一致性的方法,无需手动传递参数。

public static AsyncLocal<string> TransactionId = new AsyncLocal<string>();

public async Task ProcessTransactionAsync()
{
    TransactionId.Value = Guid.NewGuid().ToString(); // 在开始时设置一次
    await Task1Async();
    await Task2Async();
}

由于 AsyncLocal 能在异步流程中携带数据,所以它非常适合用于日志记录、调试或者任何需要共享上下文的情况。

6. 高级异步代码中的错误处理

高级异步编程通常会涉及处理多个任务,其中单个任务可能会独立失败。你可以有选择性地处理异常,而不是让一个任务的失败导致所有任务都取消。以下是一个从多个数据源加载数据的场景,其中有些数据源可能加载失败,但你仍然希望处理那些加载成功的数据。

public async Task LoadDataAsync()
{
    var tasks = new List<Task>
    {
        LoadFromDatabaseAsync(),
        LoadFromApiAsync(),
        LoadFromFileAsync()
    };

    var results = await Task.WhenAll(tasks.Select(task => 
        task.ContinueWith(t => t.Exception == null))); // 只处理成功的任务
}

在这个示例中,即便其他任务失败了,每个任务也会继续执行,这样你就可以管理部分成功的情况,并有选择性地处理异常,而不会让整个流程停止。

掌握异步编程以提升性能

更深入地钻研异步编程能让你从容应对复杂的、高负载的应用程序。无论是使用 SemaphoreSlim 来限制资源、即时取消任务,还是使用 Task.WhenAll 进行并行执行,这些技术都有助于你构建更健壮、响应更迅速且扩展性良好的应用程序。

异步编程可能需要一些练习才能熟练掌握,但付出这些努力是非常值得的,特别是对于对性能要求苛刻的应用程序而言。所以,运用这些高级异步技术吧,你会发现自己开发出来的应用程序不仅运行顺畅,而且能轻松应对高负载需求。

相关留言评论
昵称:
邮箱:
阅读排行