在 .Net Core 中使用 Task.WhenAll 提高 UI 性能

科技   2024-12-01 06:46   上海  


Task.WhenAll可以通过并行而不是顺序运行任务来显著提高应用程序的性能。

这对于网络请求、文件I/O和数据库查询等I/O密集型操作特别有用。

使用Task.WhenAll的性能优势
使用Task.WhenAll通过以下方式改善性能:

  1. 最大化资源利用:
    并发运行的任务可以更好地利用CPU和IO资源,从而加快整体执行速度。

  2. 减少等待时间:
    Task.WhenAll允许多个任务同时运行,而不是等待一个任务完成后再启动下一个,从而减少总等待时间。

  3. 避免线程阻塞:
    使用Task.WhenAll的异步编程可以防止线程阻塞,使应用程序更具响应性和可扩展性。

  4. 提高吞吐量:
    并行运行任务可以提高应用程序的吞吐量,使其能够在更短的时间内处理更多工作。

示例场景:
假设你有三个任务,每个任务需要2秒钟完成。如果按顺序运行,总时间将为6秒。但是,如果使用Task.WhenAll并行运行它们,假设没有资源争用问题,总时间将只需2秒。

Task.WhenAll是.NET Core中任务并行库(TPL)提供的一个方法,它用于创建一个任务,该任务在所有提供的任务完成时完成。在需要并行运行多个异步操作并等待所有操作完成后再继续的场景中特别有用。

让我们讨论实际场景

  1. 并发执行多个数据库查询
    假设我们有n个SQL查询,我们想并行执行,那么我们可以使用Task.WhenAll。

在这种情况下,UI将比按顺序执行这些SQL获得更快的响应。

using System.Data.SqlClient;
using System.Threading.Tasks;

public class DatabaseService
{
private string connectionString = "your_connection_string";

public async Task ExecuteQueriesAsync(string[] queries)
{
var tasks = queries.Select(query => ExecuteQueryAsync(query)).ToArray();
await Task.WhenAll(tasks);
}

private async Task ExecuteQueryAsync(string query)
{
using (var connection = new SqlConnection(connectionString))
{
await connection.OpenAsync();
using (var command = new SqlCommand(query, connection))
{
await command.ExecuteNonQueryAsync();
}
}
}
}
  1. 并发执行多个I/O操作
    假设我们需要并行读取多个文件,那么我们可以使用Task.WhenAll。

考虑以下代码:

using System.IO;
using System.Threading.Tasks;

public class IoService
{
public async Task ReadFiles(string[] filePaths)
{
var tasks = filePaths.Select(filePath => Task.Run(() => File.ReadAllText(filePath))).ToArray();
var results = await Task.WhenAll(tasks);

foreach (var content in results)
{
Console.WriteLine(content);
}
}
}

我们正在并行读取文件,UI将快速获得响应。

  1. 不同端点的并发API调用
    假设我们想并行读取多个API。

我们使用HttpClient对象与端点建立连接。

using System.Net.Http;
using System.Threading.Tasks;

public class ApiService
{
private static readonly HttpClient httpClient = new HttpClient();

public async Task FetchDataFromEndpointsAsync(string[] endpoints)
{
var tasks = endpoints.Select(endpoint => httpClient.GetStringAsync(endpoint)).ToArray();
var results = await Task.WhenAll(tasks);

foreach (var result in results)
{
Console.WriteLine(result);
}
}
}
  1. 并发下载多个文件
    假设我们在数据库、Blob存储或某个文件系统中有多个文件,我们想独立下载,那么我们可以在这里使用Task.WhenAll。

优点:与顺序下载相比,并发下载文件可以显著减少总下载时间。

using System.Net.Http;
using System.Threading.Tasks;

public class DownloadService
{
private static readonly HttpClient httpClient = new HttpClient();

public async Task DownloadFilesAsync(string[] urls, string destinationFolder)
{
var tasks = urls.Select(url => DownloadFileAsync(url, destinationFolder)).ToArray();
await Task.WhenAll(tasks);
}

private async Task DownloadFileAsync(string url, string destinationFolder)
{
var fileName = Path.Combine(destinationFolder, Path.GetFileName(url));
var data = await httpClient.GetByteArrayAsync(url);
await File.WriteAllBytesAsync(fileName, data);
}
}
  1. 执行多个后台任务

场景:应用程序需要执行多个独立的后台任务,如数据清理、日志记录和报告生成。

优点:并发运行后台任务确保它们更快完成,使应用程序能够更快地处理新任务。

using System.Threading.Tasks;

public class BackgroundTaskService
{
public async Task RunBackgroundTasksAsync()
{
var tasks = new Task[]
{
Task.Run(() => CleanupData()),
Task.Run(() => LogActivity()),
Task.Run(() => GenerateReports())
};

await Task.WhenAll(tasks);
}

private void CleanupData()
{
// Data cleanup logic
}

private void LogActivity()
{
// Logging logic
}

private void GenerateReports()
{
// Reporting logic
}
}

Task.WhenAll的异常处理
使用Task.WhenAll时,正确处理异常很重要,因为任何任务都可能失败,你需要一种方法来管理这些失败。如果任何任务失败,Task.WhenAll本身将抛出AggregateException。以下是如何以健壮的方式处理异常。

Task.WhenAll的异常处理
关键点:

  1. AggregateException:
    当Task.WhenAll完成时,如果任何任务失败,它会抛出一个AggregateException。这个异常包含所有任务抛出的个别异常。

  2. 处理单个任务异常:
    你可以遍历AggregateException的InnerExceptions属性来处理每个单独的异常。

  3. 使用ContinueWith进行异常处理:
    使用ContinueWith在异常发生时立即处理。

示例1:Task.WhenAll的基本异常处理
这是一个演示如何处理使用Task.WhenAll时异常的简单示例:

using System;
using System.Linq;
using System.Threading.Tasks;

public class ExceptionHandlingService
{
public async Task ProcessTasksWithExceptionHandlingAsync()
{
var tasks = new Task[]
{
Task.Run(() => PerformTask(1)),
Task.Run(() => PerformTask(2)),
Task.Run(() => PerformTask(3))
};

try
{
await Task.WhenAll(tasks);
}
catch (AggregateException ex)
{
foreach (var innerException in ex.InnerExceptions)
{
Console.WriteLine($"Task failed with: {innerException.Message}");
}
}
}

private void PerformTask(int taskNumber)
{
if (taskNumber == 2)
{
throw new InvalidOperationException($"Task {taskNumber} encountered an error.");
}
Console.WriteLine($"Task {taskNumber} completed successfully.");
}
}

public class Program
{
public static async Task Main(string[] args)
{
var service = new ExceptionHandlingService();
await service.ProcessTasksWithExceptionHandlingAsync();
}
}

示例2:使用ContinueWith立即处理异常
这个示例演示了如何使用ContinueWith在异常发生时立即处理:

using System;
using System.Linq;
using System.Threading.Tasks;

public class ImmediateExceptionHandlingService
{
public async Task ProcessTasksWithImmediateExceptionHandlingAsync()
{
var tasks = new Task[]
{
Task.Run(() => PerformTask(1)).ContinueWith(HandleException, TaskContinuationOptions.OnlyOnFaulted),
Task.Run(() => PerformTask(2)).ContinueWith(HandleException, TaskContinuationOptions.OnlyOnFaulted),
Task.Run(() => PerformTask(3)).ContinueWith(HandleException, TaskContinuationOptions.OnlyOnFaulted)
};

try
{
await Task.WhenAll(tasks);
}
catch (AggregateException ex)
{
foreach (var innerException in ex.InnerExceptions)
{
Console.WriteLine($"Task failed with: {innerException.Message}");
}
}
}

private void PerformTask(int taskNumber)
{
if (taskNumber == 2)
{
throw new InvalidOperationException($"Task {taskNumber} encountered an error.");
}
Console.WriteLine($"Task {taskNumber} completed successfully.");
}

private void HandleException(Task task)
{
if (task.Exception != null)
{
foreach (var ex in task.Exception.InnerExceptions)
{
Console.WriteLine($"Handled immediately: {ex.Message}");
}
}
}
}

public class Program
{
public static async Task Main(string[] args)
{
var service = new ImmediateExceptionHandlingService();
await service.ProcessTasksWithImmediateExceptionHandlingAsync();
}
}

说明

  1. 基本异常处理:
    在第一个示例中,Task.WhenAll在try-catch块中被等待。如果任何任务抛出异常,它将在catch块中被捕获,并且每个InnerException都会被单独处理。

  2. 使用ContinueWith的即时异常处理:
    在第二个示例中,ContinueWith与TaskContinuationOptions.OnlyOnFaulted选项一起使用,以在异常发生后立即处理。这允许你在任务失败时立即记录或处理异常,同时仍然使用Task.WhenAll等待所有任务完成。


  3. 如果你喜欢我的文章,请给我一个赞!谢谢

架构师老卢
资深软件架构师, 分享编程、软件设计经验, 教授前沿技术, 分享技术资源(每天发布电子书),每天进步一点点...
 最新文章