CodeGym /课程 /C# SELF /管理多个任务

管理多个任务

C# SELF
第 60 级 , 课程 2
可用

1. Task.WhenAll:同时等待所有任务

现实中很少只有一件事要做。你起床时看下表、洗个澡唱会儿歌、穿衣服听音乐,同时还在煮咖啡。或者在电脑上,你可能同时上传几个文件、发送一堆网络请求、按照不同方向计算大量数据。所有这些操作都可以(也应该)并行执行,这样就不会浪费用户的时间。我们不想一个接一个地等每个任务完成!怎么组织这种“管弦乐”?怎么判断什么时候全部都完成了?或者相反,怎么知道谁先完成?

Task.WhenAllTask.WhenAny 就是为这些场景准备的工具。

WhenAll 方法

Task.WhenAll 是类 Task 的静态方法,它接收一个任务集合并返回一个新任务,这个新任务只有在所有传入任务都完成后才会结束。就像考试时你等着所有同学交完答卷才离开考场一样。

方法签名

Task Task.WhenAll(params Task[] tasks)
Task<TResult[]> Task.WhenAll<TResult>(params Task<TResult>[] tasks)

有返回结果的任务(Task<TResult>)和“空”的任务(Task)两种版本。

最简单的例子:等待所有文件下载完

我们来异步下载三个文件。为简单起见,用延迟(Task.Delay)来模拟下载。示例是真实可运行的代码。

using System;
using System.Threading.Tasks;

class Program
{
    // 模拟延迟下载文件,返回文件名
    static async Task<string> DownloadFileAsync(string fileName)
    {
        Console.WriteLine($"► 开始下载:{fileName}");
        await Task.Delay(2000); // 等待 2 秒
        Console.WriteLine($"✓ 下载完成:{fileName}");
        return fileName;
    }

    static async Task Main()
    {
        var files = new[] { "fileA.txt", "fileB.txt", "fileC.txt" };

        // 同时启动所有下载
        var downloadTasks = new Task<string>[files.Length];
        for (int i = 0; i < files.Length; i++)
        {
            downloadTasks[i] = DownloadFileAsync(files[i]);
        }

        // 等待所有下载完成
        string[] results = await Task.WhenAll(downloadTasks);

        Console.WriteLine($"所有下载完成!列表:{string.Join(", ", results)}");
    }
}

这里发生了什么?

  • 我们不是按顺序等待每个任务。所有三个同时启动(异步)。
  • Task.WhenAll(downloadTasks) 返回一个任务,这个任务只有在所有子任务都完成后才会完成。
  • 之后可以处理所有任务的结果 —— 使用它们、输出到屏幕、继续传递给别的逻辑。

类比

这就像你派了三个快递员去送三个包裹,只有当所有人都把包裹送到客户手中,你才向老板报告“都送完了”。

Task.WhenAll 的工作流程示意

sequenceDiagram
    participant Program
    participant 任务1
    participant 任务2
    participant 任务3

    Program->>任务1: 启动
    Program->>任务2: 启动
    Program->>任务3: 启动
    任务1-->>Program: 完成(可能比其他提前)
    任务2-->>Program: 完成
    任务3-->>Program: 完成
    Program->>Program: Task.WhenAll 完成,所有结果可用

如果其中一个任务抛出异常会怎样?

Task.WhenAll 不会在某个任务出错时立即中断。它会等所有任务都结束。如果至少有一个任务发生异常,最终任务会处于 Faulted 状态,并且包含所有发生的异常的集合。

示例

static async Task<string> MayThrowAsync(string fileName)
{
    await Task.Delay(500);
    if (fileName == "fileB.txt")
        throw new Exception("下载 fileB.txt 时出错");
    return fileName;
}

static async Task Main()
{
    var files = new[] { "fileA.txt", "fileB.txt", "fileC.txt" };
    var downloadTasks = files.Select(MayThrowAsync).ToArray();
    try
    {
        string[] results = await Task.WhenAll(downloadTasks);
        Console.WriteLine($"一切顺利:{string.Join(", ", results)}");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"至少有一个任务出错:{ex.Message}");
    }
}

如果发生异常,想查看所有内部错误,请使用 AggregateException.InnerExceptions
官方文档: Task.WhenAll docs

2. Task.WhenAny:等待第一个完成的任务

Task.WhenAny 也是静态方法,但它在任意一个传入任务进入“完成”状态(成功或失败)时就结束。它返回第一个完成的任务的引用。

签名

Task<Task> Task.WhenAny(params Task[] tasks)
Task<Task<TResult>> Task.WhenAny<TResult>(params Task<TResult>[] tasks)

类比

谁先把蛋糕烤好谁就厉害,有时候你只需要知道哪个服务器最快响应,然后用它的结果。

示例:谁更快?

using System;
using System.Threading.Tasks;

class Program
{
    // 模拟不同速度的请求
    static async Task<string> RequestAsync(string name, int delay)
    {
        await Task.Delay(delay);
        return $"{name} 完成耗时 {delay} 毫秒";
    }

    static async Task Main()
    {
        var taskA = RequestAsync("A", 1000);
        var taskB = RequestAsync("B", 700);    // 最快
        var taskC = RequestAsync("C", 1500);

        // 等待第一个完成的任务
        Task<string> finished = await Task.WhenAny(taskA, taskB, taskC);

        Console.WriteLine($"最先完成的是:{finished.Result}");
    }
}

WhenAny 我们可以知道哪个任务先到达终点。之后可以取消其余任务,例如用 CancellationToken(下一些讲会讲)。

为什么 WhenAny 返回的是任务而不是结果?

因为方法不知道哪个任务会先完成:它无法提前知道哪个任务的返回类型是什么。所以它返回完成的任务本身 —— 我们需要自己取出结果:用 await 或访问该任务的 Result

WhenAll vs WhenAny:对照说明

方法 何时触发 返回什么 典型场景
Task.WhenAll
全部 完成时 任务(Task/Task<T[]> 等所有响应或所有文件,聚合结果
Task.WhenAny
当任意一个任务完成时 最先完成的那个任务 使用第一个结果,取消其余任务

3. 结合 WhenAllWhenAny — 真实场景

有时需要先等任意一个任务完成,再等全部完成;或者反过来。

示例:两个服务器之间的“乒乓”

假设你同时向两个克隆服务器发起请求,以防其中一个变慢。但你会使用第一个返回的结果。

static async Task Main()
{
    var fastServer = RequestAsync("快速服务器", 400);   // 400 ms
    var slowServer = RequestAsync("慢速服务器", 2000); // 2000 ms

    var completed = await Task.WhenAny(fastServer, slowServer);
    Console.WriteLine(await completed);

    // *可选*:这里可以通过 CancellationToken 取消尚未完成的任务
}

示例:只有在所有数据都加载完后才开始处理

你的应用有三个数据源。只有当三个都加载完,才应该进行处理:

static async Task Main()
{
    var task1 = DownloadFileAsync("a.txt");
    var task2 = DownloadFileAsync("b.txt");
    var task3 = DownloadFileAsync("c.txt");

    var all = await Task.WhenAll(task1, task2, task3);

    ProcessFiles(all[0], all[1], all[2]);
}

4. 使用 Task.WhenAllTask.WhenAny 的常见错误

错误 №1:以为 WhenAll 在第一个任务完成时就结束。
新手常以为 Task.WhenAll 会在第一个任务完成时返回。其实它会等待所有任务完成。

错误 №2:忽视 WhenAll 中的异常。
如果某个任务抛出异常,最终任务会处于 Faulted 状态。不处理 AggregateException.InnerExceptions 可能会漏掉重要错误。

错误 №3:在 WhenAny 中不检查就直接访问 Result
如果第一个完成的任务本身抛了异常,访问 Result 会抛出异常。访问之前请检查 Task.IsFaulted

错误 №4:把任务串行化而不是并行化。
例如在循环里对每个任务都用 await,而不是用 Task.WhenAll,会大幅降低性能。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION