在现代 .NET 应用程序开发中,异步编程(Asynchronous Programming)已成为提升性能、改善响应能力和充分利用多核处理器的关键技术。async
和 await
关键字极大地简化了异步代码的编写,而 Task
类则是这一模型的核心。在处理多个并发操作时,Task.WhenAll
方法是一个强大且常用的工具。本文将深入探讨 Task.WhenAll
的工作原理、使用场景、最佳实践以及需要注意的陷阱。
什么是 Task.WhenAll
?
Task.WhenAll
是 Task
类提供的一个静态方法,用于等待多个任务(Task
或 Task<T>
)全部完成。它接收一个任务集合(如 IEnumerable<Task>
或 Task[]
),并返回一个新的 Task
或 Task<TResult[]>
。当传入的任务全部成功完成时,返回的任务才会完成。如果其中任何一个任务因异常而失败,返回的任务也会以异常状态完成。
基本语法
// 等待多个无返回值的任务完成
Task WhenAll(params Task[] tasks);
Task WhenAll(IEnumerable<Task> tasks);
// 等待多个有返回值的任务完成
Task<TResult[]> WhenAll<TResult>(params Task<TResult>[] tasks);
Task<TResult[]> WhenAll<TResult>(IEnumerable<Task<TResult>> tasks);
为什么需要 Task.WhenAll
?
在没有 Task.WhenAll
的情况下,我们可能会这样处理多个异步操作:
// ❌ 错误方式:顺序执行,效率低下
var result1 = await GetDataAsync(url1);
var result2 = await GetDataAsync(url2);
var result3 = await GetDataAsync(url3);
// 所有操作是串行的,总耗时约等于各操作耗时之和
或者使用 Task.WhenAny
,但它只等待第一个完成的任务,无法满足“全部完成”的需求。
Task.WhenAll
允许我们并发地启动所有操作,然后等待它们全部结束,从而显著减少总执行时间。
✅ 正确方式:使用 Task.WhenAll
// 启动所有任务(并发)
var task1 = GetDataAsync(url1);
var task2 = GetDataAsync(url2);
var task3 = GetDataAsync(url3);
// 等待所有任务完成
await Task.WhenAll(task1, task2, task3);
// 所有任务都已完成,可以安全地获取结果
var result1 = await task1; // 不会阻塞,因为任务已结束
var result2 = await task2;
var result3 = await task3;
或者更简洁地:
var tasks = new[]
{
GetDataAsync(url1),
GetDataAsync(url2),
GetDataAsync(url3)
};
await Task.WhenAll(tasks);
// 结果按任务在数组中的顺序排列
var results = await Task.WhenAll(tasks); // 对于有返回值的任务
核心优势
- 性能提升:通过并发执行,总耗时通常接近于最慢的那个任务的耗时,而不是所有任务耗时之和。
- 代码简洁:避免了复杂的
Task.ContinueWith
链或手动管理多个TaskCompletionSource
。 - 异常处理集中:所有任务的异常都可以在
await Task.WhenAll(...)
处被捕获(通常包装在AggregateException
中)。 - 结果聚合:对于
Task<T>
,WhenAll
直接返回一个包含所有结果的数组,顺序与输入任务一致。
重要注意事项与最佳实践
1. 任务必须已经启动
Task.WhenAll
等待的是已经启动的任务。确保你在调用 Task.WhenAll
之前已经启动了所有任务(即调用了异步方法但没有 await
它)。
// ✅ 正确:任务已启动
var task1 = SomeAsyncOperation(); // 启动任务
var task2 = AnotherAsyncOperation(); // 启动任务
await Task.WhenAll(task1, task2);
// ❌ 错误:任务未启动(SomeAsyncOperation 返回的是 Task,但未执行)
// await Task.WhenAll(SomeAsyncOperation(), AnotherAsyncOperation());
// 这行代码本身会启动任务,但写法不直观,建议分开写。
2. 异常处理
Task.WhenAll
返回的任务在任何一个输入任务失败时都会失败。异常通常被包装在 AggregateException
中。推荐使用 try-catch
块来处理:
try
{
await Task.WhenAll(task1, task2, task3);
}
catch (Exception ex)
{
// ex 可能是 AggregateException 或单个异常(.NET 4.5+ 有时会扁平化)
// 检查 ex.InnerException 或 ex.Flatten().InnerExceptions 来获取具体异常
Console.WriteLine($"一个或多个任务失败: {ex.Message}");
// 可以遍历所有任务检查其状态
foreach (var task in new[] { task1, task2, task3 })
{
if (task.IsFaulted)
{
Console.WriteLine($"任务 {task.Id} 失败: {task.Exception?.InnerException?.Message}");
}
}
}
3. 性能考量:避免不必要的 Task.WhenAll
如果任务数量很少(比如1-2个),直接 await
每个任务可能更清晰。Task.WhenAll
在处理多个(例如3个以上)独立任务时优势最明显。
4. 与 Task.WhenAny
的区别
Task.WhenAll
: 等待所有任务完成。Task.WhenAny
: 等待任何一个任务完成。适用于“竞态”场景,比如从多个数据源获取数据,取最快返回的结果。
5. 内存与资源管理
启动大量并发任务可能会耗尽系统资源(如线程池线程、网络连接、文件句柄)。考虑使用 SemaphoreSlim
或 Parallel.ForEachAsync
(C# 11+) 来限制并发度。
后续其他文章再展开讲解(这里先挖个坑~~~~,可私信我填坑,我怕忘记了。)
6. 创建一个带有默认值的已完成任务!
有的时候,在进入其他分支的时候,某个task可能是null,这样Task.WhenAll等待的时候就会报错,我们需要创建一个带有默认值的已完成任务
.
比如:
Task task3 = Task.FromResult(default(bool))
- default(bool)
default 是 C# 中的一个关键字,用于获取类型的默认值。
对于值类型 bool,其默认值是 false。
所以,default(bool) 等价于 false。 - Task.FromResult(T result)
这是 Task 类的一个静态方法。
它接收一个类型为 T 的参数 result。
它返回一个 Task 对象,这个对象的状态是已完成(RanToCompletion),并且其 Result 属性的值就是传入的 result。
这个方法非常高效,因为它不会创建一个新的线程或启动任何异步工作;它只是包装一个已经存在的值到一个 Task 的壳子里。 - Task.FromResult(default(bool))
将两者结合起来,Task.FromResult(default(bool)) 创建并返回一个 Task。
这个 Task 立即完成。
当你 await 这个任务时,你会立即得到 false。
7 混合处理无返回值与有返回值的任务
当我们需要同时等待一个无返回值的 Task 和一个有返回值的 Task 时,Task.WhenAll 仍然适用,但获取返回值该怎么做呢?
实现方式
Task.WhenAll 可以接收混合类型的任务(Task 和 Task),但返回的是 Task 而非 Task<T[]>。因此,我们需要通过原始的有返回值任务的引用获取结果。
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
// 无返回值的任务
Task task1 = Task.Run(() =>
{
Console.WriteLine("任务1(无返回值)开始");
Task.Delay(2000).Wait();
Console.WriteLine("任务1完成");
});
// 有返回值的任务(返回bool)
Task<bool> task2 = Task.Run(() =>
{
Console.WriteLine("任务2(有返回值)开始");
Task.Delay(1500).Wait();
Console.WriteLine("任务2完成");
return true; // 返回bool结果
});
// 同时等待两个任务完成
await Task.WhenAll(task1, task2);
// 单独获取有返回值的任务结果
bool result = await task2; // 更推荐的异步方式
//bool result = task2.Result; 这么写也是可以的!!!!!!
Console.WriteLine($"任务2的返回值:{result}");
Console.WriteLine("所有任务都已完成");
}
}
最后给一个实际的例子
/// <summary>
/// 采集图片进行推理
/// </summary>
/// <param name="index">对应的相机:0 正面相机; 1 反面相机</param>
/// <returns></returns>
async Task<bool> RunOne(int index)
{
Stopwatch 计时器 = new Stopwatch();
List<YOLOData> data = new List<YOLOData>();
CameraConfig info;
GraphicInfo graphic;
//YOLODetection yolo;
HObject Hobj;
try
{
info = GlobalData.Instance.saveInfo.NeedOpenedCameraList[index];
graphic = GlobalData.Instance.saveInfo.Graphics.First(t=>t.SerialNumber == info.SerialNumber);
//yolo = graphic.yolo;
}
catch (Exception)
{
throw new Exception($"NeedOpenedCameraList或者graphic,未找到第{index}个相机");
}
Task task1, task2;
Task<bool> task3 = Task.FromResult(default(bool));
try
{
计时器.Restart();
//----触发运动(从初始位置到结束位置)
await motionCard.PmoveEx(axisConfigInfo, axisConfigInfo.Positions[0].Value);
logger.Info($"开始运动到{axisConfigInfo.Positions[0].Value}");
//触发一次采集!
CameraService.Snap(info.SerialNumber);
//----触发运动(从结束位置到初始位置)
task1 = motionCard.PmoveEx(axisConfigInfo, axisConfigInfo.Positions[1].Value);
logger.Info($"结束运动到{axisConfigInfo.Positions[1].Value}");
//采集图片
Hobj = CameraService.GetHImage(info.SerialNumber, info.Timeout);
logger.Info($"采集到图片!");
计时器.Stop();
//----触发运动(从初始位置到结束位置)
task2 = motionCard.PmoveEx(axisConfigInfo, axisConfigInfo.Positions[0].Value);
logger.Info($"回到开始位置!{axisConfigInfo.Positions[0].Value}");
}
catch (Exception ex)
{
MessageBox.Show($"运动采集失败{ex.Message}");
return false;
}
var t1 = 计时器.ElapsedMilliseconds;
bool b = false;
if (Hobj != null)
{
task3 = ImgCheck(graphic, Hobj);
}
else
{
MainWindowViewModel.PostGrowlEvent(info.SerialNumber + "相机获取图片失败", EnumAlarmType.Warning);
Task.Delay(1000).Wait();
b = false;
}
await Task.WhenAll(task1, task2, task3);
//获取结果
b = task3.Result;
logger.Info($"完成一次检测~~~~~结果为:{b}");
return b;
}
总结
Task.WhenAll
是 .NET 异步编程工具箱中不可或缺的一部分。它通过并发执行多个独立的异步操作,极大地提升了应用程序的效率和响应能力。正确理解和使用 Task.WhenAll
,能够让你的代码更加高效、简洁和健壮。
核心要点回顾:
- 并发启动,然后等待全部完成。
- 显著减少总执行时间。
- 集中处理异常和聚合结果。
- 注意任务启动时机和异常处理。
- 根据场景控制并发度。
掌握 Task.WhenAll
,让你的 .NET 应用在处理多任务时游刃有余!