Task API 完全指南:方法与属性的实战应用

03. Task API 完全指南:方法与属性的实战应用

本章 GitHub 仓库csharp-concurrency-cookbook

欢迎 Star 和 Fork!所有代码示例都可以在仓库中找到并运行。


本章导读

本文目标:系统性掌握 Task 类的核心 API,为后续深入学习 async/await 打下坚实基础。

在上一篇文章中,我们了解了 Task 是基于 ThreadPool 的抽象,是现代 .NET 并发编程的核心。本文将深入探讨 Task 类提供的各种方法和属性:

  • 创建任务:如何创建并启动一个 Task?
  • 等待任务:如何等待任务完成并获取结果?
  • 组合任务:如何组合多个任务?
  • 任务状态:如何查询任务的执行状态?
  • 常见陷阱:哪些用法容易出错?

重要提示:本文聚焦 Task API 本身,关于 async/await 的深入讨论将在下一章展开。


1️⃣ 创建任务:三种方式的选择

1.1 Task.Run:最常用的方式

作用说明Task.Run() 将一个委托(Lambda 表达式或方法)排队到 ThreadPool,并立即返回一个表示该操作的 Task 对象。任务会在线程池的某个工作线程上异步执行。它是创建 CPU 密集型任务的推荐方式,自动处理任务的启动和调度。

适用场景:将 CPU 密集型工作放到线程池执行。

// 最简单的方式:执行一个操作
Task task = Task.Run(() =>
{
    Console.WriteLine($"在线程 {Thread.CurrentThread.ManagedThreadId} 上执行");
    Thread.Sleep(1000);
});

// 带返回值的版本
Task<int> resultTask = Task.Run(() =>
{
    Thread.Sleep(1000);
    return 42;
});

int result = resultTask.Result; // 阻塞等待结果
Console.WriteLine($"结果: {result}");

核心特点

  • 立即启动,无需手动调用 Start()
  • 自动使用 ThreadPool 线程池
  • 代码简洁,是最推荐的方式

1.2 Task.Factory.StartNew:高级控制

作用说明Task.Factory.StartNew() 提供了比 Task.Run 更多的控制选项,允许指定 TaskCreationOptions(如长时间运行)、CancellationToken、以及自定义的 TaskScheduler。它也会立即启动任务,但默认行为与 Task.Run 有细微差异(如调度器的选择)。

适用场景:需要更精细的控制(如长时间运行任务、自定义调度器)。

// 普通用法(不推荐,应该用 Task.Run)
Task task1 = Task.Factory.StartNew(() =>
{
    Console.WriteLine("普通任务");
});

// 标记为长时间运行任务(会创建专用线程,不占用线程池)
Task task2 = Task.Factory.StartNew(() =>
{
    Console.WriteLine("长时间运行的任务");
    Thread.Sleep(10000);
}, TaskCreationOptions.LongRunning);

// 使用自定义调度器
Task task3 = Task.Factory.StartNew(() =>
{
    Console.WriteLine("自定义调度器");
}, CancellationToken.None,
   TaskCreationOptions.None,
   TaskScheduler.Default);

️ 关键差异

特性 Task.Run Task.Factory.StartNew
默认调度器 TaskScheduler.Default TaskScheduler.Current
嵌套任务 自动展开 需要手动展开
建议用途 日常使用 高级场景

1.3 new Task():手动控制启动

作用说明:使用 new Task() 创建任务时,任务不会自动启动,需要手动调用 Start() 方法。这允许在创建和启动之间进行额外的配置或等待特定时机。任务创建后处于 Created 状态,调用 Start() 后才会排队到线程池执行。

适用场景:需要延迟启动或特殊控制流程。

// 创建但不启动
Task task = new Task(() =>
{
    Console.WriteLine("手动启动的任务");
});

Console.WriteLine($"任务状态: {task.Status}"); // Created

// 手动启动
task.Start();

// 等待完成
task.Wait();

常见错误

// 错误:忘记调用 Start()
var task = new Task(() => Console.WriteLine("Hello"));
// task 永远不会执行!

正确做法
99% 的情况下应该使用 Task.Run,除非你明确需要延迟启动。


1.4 创建已完成的任务

作用说明:这些静态方法用于创建已经处于完成状态的 Task,无需实际执行任何异步操作。它们适用于同步路径的优化(避免不必要的异步开销)、测试场景、或需要返回固定结果的场景。

对于某些场景(如缓存、测试、优化),我们需要直接创建已完成的任务:

// Task.FromResult - 返回已成功完成的任务(带返回值)
Task<int> completedTask = Task.FromResult(42);
Console.WriteLine($"立即可用: {completedTask.Result}"); // 不会阻塞,立即返回

// Task.CompletedTask - 返回已成功完成的任务(无返回值)
Task emptyTask = Task.CompletedTask;

// Task.FromCanceled - 返回已取消的任务
CancellationTokenSource cts = new CancellationTokenSource();
cts.Cancel();
Task canceledTask = Task.FromCanceled(cts.Token);

// Task.FromException - 返回已失败的任务
Task faultedTask = Task.FromException(new InvalidOperationException("出错了"));

实战应用:接口实现的优化

public interface IDataService
{
    Task<string> GetDataAsync();
}

// 缓存实现:数据已在内存中,无需真正的异步操作
public class CachedDataService : IDataService
{
    private string _cachedData = "缓存的数据";


    public Task<string> GetDataAsync()
    {
        // 避免不必要的异步开销
        return Task.FromResult(_cachedData);
    }
}

2️⃣ 等待任务:同步等待的陷阱

2.1 Wait():阻塞等待

作用说明Wait() 方法会阻塞当前线程,直到任务完成才返回。如果任务已完成,则立即返回;如果任务尚未完成,当前线程会被挂起,进入等待状态,直到任务执行完毕。这是一个同步阻塞方法,会占用调用线程,直到任务结束。

Task task = Task.Run(() =>
{
    Thread.Sleep(2000);
    Console.WriteLine("任务完成");
});

// 阻塞当前线程,直到任务完成
task.Wait();
Console.WriteLine("继续执行");

️ 性能陷阱:为什么 .Result 和 .Wait() 会浪费线程资源?

//  错误:在 ASP.NET Core 中阻塞等待
public IActionResult GetData()
{
    var data = GetDataAsync().Result; // 浪费线程资源!
    return Ok(data);
}

//  正确:使用 async/await(第4章详解)
public async Task<IActionResult> GetData()
{
    var data = await GetDataAsync();
    return Ok(data);
}

内部实现机制:为什么会阻塞线程?

要理解为什么 .Result.Wait() 会浪费线程资源,我们需要深入了解它们的内部实现机制

1. Wait() 的内部实现原理

Wait() 方法内部使用 ManualResetEventSlim 作为同步原语来实现阻塞。简化的内部流程如下:

flowchart TD Start([调用 task.Wait]) –> CheckStatus{检查任务状态} CheckStatus –>|任务已完成| ReturnFast[立即返回<br/>不阻塞] CheckStatus –>|任务未完成| SpinWait[阶段1: SpinWait 自旋等待<br/>━━━━━━━━━━━━━━━━<br/>• 用户态循环检查任务状态<br/>• 持续约 10-30 次循环<br/>• 避免昂贵的内核态切换<br/>• 线程状态: Running 占用CPU] SpinWait –> SpinCheck{自旋期间<br/>任务完成?} SpinCheck –>|是| ReturnSpin[返回 幸运!] SpinCheck –>|否| BlockWait[阶段2: ManualResetEventSlim.Wait<br/>━━━━━━━━━━━━━━━━━━━━━━<br/>内核阻塞] BlockWait –> ThreadBlock[线程状态转换<br/>━━━━━━━━━━<br/>Running → WaitSleepJoin<br/>• OS调度器将线程移出运行队列<br/>• 线程进入内核态等待队列<br/>• 不再消耗CPU但占用线程池槽位] ThreadBlock –> Suspended[线程被挂起 等待信号<br/>━━━━━━━━━━━━━━<br/>• 线程对象在内存 ~1MB栈空间<br/>• 占用ThreadPool槽位<br/>• OS不分配CPU时间片] Suspended –> TaskComplete[任务完成事件<br/>━━━━━━━━━] TaskComplete –> SetResult[Task内部调用 SetResult] SetResult –> SignalEvent[ManualResetEventSlim.Set<br/>发出信号] SignalEvent –> WakeUp[操作系统唤醒线程] WakeUp –> ThreadReady[线程状态转换<br/>━━━━━━━━━━<br/>WaitSleepJoin → Ready → Running] ThreadReady –> ReturnWait[Wait 返回<br/>继续执行后续代码] style Start fill:#bbdefb,color:#1565c0,stroke:#1976d2,stroke-width:2px style ReturnFast fill:#c8e6c9,color:#2e7d32,stroke:#388e3c,stroke-width:2px style ReturnSpin fill:#c8e6c9,color:#2e7d32,stroke:#388e3c,stroke-width:2px style ReturnWait fill:#c8e6c9,color:#2e7d32,stroke:#388e3c,stroke-width:2px style SpinWait fill:#fff9c4,color:#f57f17,stroke:#f9a825,stroke-width:2px style BlockWait fill:#ffccbc,color:#d84315,stroke:#e64a19,stroke-width:2px style ThreadBlock fill:#ffccbc,color:#d84315,stroke:#e64a19,stroke-width:2px style Suspended fill:#ffccbc,color:#d84315,stroke:#e64a19,stroke-width:2px style SetResult fill:#e1bee7,color:#6a1b9a,stroke:#7b1fa2,stroke-width:2px style SignalEvent fill:#e1bee7,color:#6a1b9a,stroke:#7b1fa2,stroke-width:2px style WakeUp fill:#e1bee7,color:#6a1b9a,stroke:#7b1fa2,stroke-width:2px

关键实现细节

  • ManualResetEventSlim:.NET 的轻量级同步原语,内部封装了 Win32 的 WaitHandle(在 Windows 上是 CreateEvent / WaitForSingleObject
  • 两阶段等待策略
    • 自旋(SpinWait):短时间内在用户态循环检查,避免线程切换开销(适合非常快完成的任务)
    • 阻塞(Blocking):如果自旋超时,则调用内核同步原语,真正挂起线程(节省 CPU,但线程仍被占用)

2. .Result 属性的内部实现

.Result 属性的实现更简单,它本质上是:

// Task<T>.Result 的简化实现(伪代码)
public T Result
{
    get
    {
        // 1. 如果任务未完成,调用 Wait() 阻塞
        if (!IsCompleted)
        {
            InternalWait(Timeout.Infinite, default);
        }

        // 2. 如果任务出错,重新抛出异常(包装在 AggregateException 中)
        if (IsFaulted)
        {
            throw new AggregateException(Exception);
        }

        // 3. 返回结果
        return _result;
    }
}

可以看到,.Result 内部直接调用 Wait(),因此阻塞机制完全相同。

线程状态转换与资源占用分析

在第 02 章我们学到,ThreadPool 是有限的线程资源。现在让我们结合内部实现来分析 Wait() 造成的资源浪费

ASP.NET Core 中的资源浪费示例

sequenceDiagram participant Client as 客户端 participant TP as ThreadPool participant RT as 请求线程 #1 participant IO as I/O 完成端口<br/>(IOCP) participant WT as 工作线程 #2 participant DB as 数据库 Note over Client,DB: T0: 请求到达 Client->>TP: HTTP 请求 TP->>RT: 分配线程 activate RT Note right of RT: 状态: Running<br/>槽位: 1/64 Note over Client,DB: T1: 调用 GetDataAsync().Result RT->>RT: 开始 SpinWait Note right of RT: 用户态自旋<br/>占用 CPU Note over Client,DB: T2: SpinWait 超时,进入内核阻塞 RT->>RT: ManualResetEventSlim.Wait() Note right of RT: ️ 状态: Running → WaitSleepJoin<br/>━━━━━━━━━━━━━━<br/>资源占用:<br/>• ThreadPool 槽位: 1/64<br/>• 栈内存: ~1MB<br/>• 内核句柄: 1 个 RT->>IO: 提交异步 I/O Note right of IO: 不占用线程 Note over Client,DB: T2~T4: 请求线程被阻塞(浪费!) Note right of RT: ️ 线程什么都不做<br/>但占用 ThreadPool 槽位 Note over Client,DB: T3: I/O 操作完成 DB–>>IO: 返回数据 IO->>TP: IOCP 完成通知 TP->>WT: 分配工作线程 activate WT WT->>RT: ManualResetEventSlim.Set() Note right of RT: 发出信号 Note over Client,DB: T4: 唤醒被阻塞的线程 Note right of RT: 状态: WaitSleepJoin<br/>→ Ready → Running RT->>RT: Wait() 返回 Note over Client,DB: T5: 返回 HTTP 响应 RT->>Client: 响应数据 deactivate RT RT->>TP: 归还线程 deactivate WT Note over Client,DB: 【问题分析】<br/>━━━━━━━━━━━━━━━━━━━━━━━<br/>• T2~T4 期间请求线程空闲但占用槽位<br/>• 100 并发 = 100 个被阻塞的线程<br/>• ThreadPool 耗尽 → 新请求排队<br/>• 高延迟、低吞吐量、超时风险

性能影响分析

资源占用对比

资源类型 单个阻塞线程的占用 100 个并发请求的占用
ThreadPool 槽位 1 个(持续占用) 100 个(可能耗尽 ThreadPool)
线程栈内存 ~1MB ~100MB
内核对象 1 个 HANDLE 句柄 100 个 HANDLE
CPU 利用率 0%(空闲但不释放) 0%(100 个线程都在等待)

真实世界的影响

在 ASP.NET Core 应用中,假设每个数据库查询耗时 100ms:

  • ThreadPool 默认最大线程数:通常为 CPU 核心数的倍数(如 64 个)
  • 理论吞吐量:64 个线程 ÷ 0.1秒 = 640 请求/秒
  • 实际问题
    • 当并发请求超过 64 个时,新请求必须排队等待线程释放
    • 排队导致延迟增加,用户体验下降
    • 严重时可能导致请求超时、服务不可用

为什么会造成性能崩溃?

  1. 线程饥饿:ThreadPool 的线程被大量阻塞操作占据,无法处理新请求
  2. 内存浪费:每个阻塞线程占用 ~1MB 栈空间,但完全空闲
  3. 延迟雪崩:排队等待的请求越来越多,延迟指数级增长
  4. 资源死锁风险:在复杂场景下,可能导致所有线程互相等待

提示:这就是为什么在高并发场景下,绝对不能使用 .Result.Wait() 阻塞等待异步操作。正确的做法是使用 async/await,这将在第 04 章详细讲解。

扩展阅读:关于 ThreadPool 工作原理和 I/O 完成端口(IOCP)机制,请参考第 02 章《深入理解 ThreadPool》。


2.2 Wait(timeout):带超时的等待

作用说明Wait(timeout)Wait() 的超时版本,它会阻塞当前线程,但最多等待指定的时间(毫秒)。如果任务在超时时间内完成,方法返回 true;如果超时了任务仍未完成,方法返回 false,但任务会继续在后台执行,不会被取消或中止。

Task longTask = Task.Run(() =>
{
    Thread.Sleep(5000);
});

// 等待最多 2 秒
bool completed = longTask.Wait(2000);

if (completed)
{
    Console.WriteLine("任务已完成");
}
else
{
    Console.WriteLine("等待超时");
    // 注意:任务仍在后台继续执行
}

2.3 Task.WaitAll:等待所有任务

作用说明Task.WaitAll() 会阻塞当前线程,直到所有传入的任务都完成(包括成功、失败或取消)才返回。多个任务是并发执行的,总耗时等于耗时最长的那个任务。如果任何一个任务抛出异常,WaitAll 会等待所有任务完成后,将所有异常包装在 AggregateException 中抛出。

Task task1 = Task.Run(() => Thread.Sleep(1000));
Task task2 = Task.Run(() => Thread.Sleep(2000));
Task task3 = Task.Run(() => Thread.Sleep(1500));

// 阻塞直到所有任务完成(取最长时间)
Task.WaitAll(task1, task2, task3);
Console.WriteLine("所有任务完成"); // 大约 2 秒后输出(不是 4.5 秒)

带超时的版本

// 最多等待 3 秒,如果超时返回 false,但所有任务会继续执行
bool allCompleted = Task.WaitAll(new[] { task1, task2, task3 }, 3000);
if (!allCompleted)
{
    Console.WriteLine("等待超时,但任务仍在后台执行");
}

2.4 Task.WaitAny:等待任意一个

作用说明Task.WaitAny() 会阻塞当前线程,直到传入的任务中任意一个完成(无论成功、失败还是取消)就立即返回。返回值是第一个完成的任务在数组中的索引(从 0 开始)。重要:方法返回后,其他未完成的任务不会被取消或中止,它们会继续在后台执行,直到完成或程序退出。

Task<int> task1 = Task.Run(async () =>
{
    await Task.Delay(2000);
    Console.WriteLine("任务 1 完成");
    return 1;
});

Task<int> task2 = Task.Run(async () =>
{
    await Task.Delay(1000);
    Console.WriteLine("任务 2 完成");
    return 2;
});

Task<int> task3 = Task.Run(async () =>
{
    await Task.Delay(1500);
    Console.WriteLine("任务 3 完成");
    return 3;
});

// 返回第一个完成的任务的索引
int index = Task.WaitAny(task1, task2, task3);
Console.WriteLine($"任务 {index + 1} 先完成"); // 任务 2 先完成(1 秒后)

// 注意:此时任务 1 和任务 3 仍在后台执行
// 它们会在 1.5 秒和 2 秒后继续输出 "任务 3 完成" 和 "任务 1 完成"

获取第一个完成的任务的结果

Task<int>[] tasks = new[] { task1, task2, task3 };
int index = Task.WaitAny(tasks);
Task<int> firstCompleted = tasks[index];
int result = firstCompleted.Result; // 安全访问,因为任务已完成
Console.WriteLine($"第一个结果: {result}");

实战应用:超时模式

Task<string> dataTask = GetDataFromSlowServiceAsync();
Task timeoutTask = Task.Delay(5000); // 5 秒超时

int index = Task.WaitAny(dataTask, timeoutTask);
if (index == 0)
{
    Console.WriteLine($"获取到数据: {((Task<string>)dataTask).Result}");
}
else
{
    Console.WriteLine("超时!");
}

2.5 GetAwaiter:等待器的底层机制

作用说明GetAwaiter() 返回一个 TaskAwaiterTaskAwaiter<T>,这是 await 关键字的底层实现机制。通常不需要直接使用,但在特殊场景下(如同步方法中调用异步方法)会用到。

// 等价于 await task
TaskAwaiter<int> awaiter = task.GetAwaiter();
int result = awaiter.GetResult();

// 等价于 await task.ConfigureAwait(false)
ConfiguredTaskAwaitable<int>.ConfiguredTaskAwaiter awaiter = 
    task.ConfigureAwait(false).GetAwaiter();
int result = awaiter.GetResult();

与 .Result 的区别

方式 异常处理 死锁风险 推荐度
.Result 包装为 AggregateException 高(在 UI 线程) 不推荐
.GetAwaiter().GetResult() 直接抛出原始异常 高(在 UI 线程) ️ 特殊场景
.ConfigureAwait(false).GetAwaiter().GetResult() 直接抛出原始异常 较低(不捕获上下文) ️ 相对安全
await 直接抛出原始异常 强烈推荐

示例:异常处理的区别

async Task ThrowExceptionAsync()
{
    await Task.Delay(100);
    throw new InvalidOperationException("Something went wrong");
}

//  使用 .Result:捕获 AggregateException
try
{
    ThrowExceptionAsync().Result;
}
catch (AggregateException ex)
{
    Console.WriteLine($"外层异常: {ex.GetType().Name}");
    Console.WriteLine($"内层异常: {ex.InnerException?.GetType().Name}");
    // 输出:
    // 外层异常: AggregateException
    // 内层异常: InvalidOperationException
}

//  使用 GetAwaiter().GetResult():直接捕获原始异常
try
{
    ThrowExceptionAsync().GetAwaiter().GetResult();
}
catch (InvalidOperationException ex)  // 直接捕获原始异常!
{
    Console.WriteLine($"异常: {ex.GetType().Name}");
    // 输出:
    // 异常: InvalidOperationException
}

️ 重要警告

  • GetAwaiter().GetResult() 仍然是阻塞调用(会占用线程资源)
  • 在 UI 线程使用仍然有死锁风险
  • 最佳实践:将整个调用链改为 async/await

何时可以使用

  • 遗留代码迁移(无法改为 async)
  • Main 方法(.NET Framework,.NET 6+ 可以用 top-level statements)
  • 同步接口的实现(如 IDisposable.Dispose,但更推荐 IAsyncDisposable

详细原理:关于 TaskAwaiter、同步上下文、死锁机制的详细讲解,会在后面章节进行。


2.6 ConfigureAwait:配置等待上下文

作用说明ConfigureAwait(bool continueOnCapturedContext) 控制 await 之后的代码是否要返回原始上下文(如 UI 线程)执行。这是 Task API 的一个重要方法,但其深入原理涉及 SynchronizationContext,将在第 05 章详细讲解。

// 基本用法
await SomeTaskAsync().ConfigureAwait(false);  // 不捕获上下文
await SomeTaskAsync().ConfigureAwait(true);   // 捕获上下文(默认)

简要说明

  • ConfigureAwait(false)

    • 不返回原始上下文(如 UI 线程)
    • 性能更高(避免线程切换)
    • 库代码推荐使用此选项
  • ConfigureAwait(true)(默认):

    • 返回原始上下文执行
    • UI 代码必须使用此选项(或不写,默认就是 true)
    • 例如:await 后需要更新 UI 控件

示例对比

// UI 代码(WPF/WinForms)
private async void Button_Click(object sender, EventArgs e)
{
    //  正确:需要回到 UI 线程更新控件
    string data = await GetDataAsync();  // 默认 ConfigureAwait(true)
    textBox.Text = data;  // 安全:在 UI 线程执行
}

// 库代码
public async Task<string> GetDataAsync()
{
    //  正确:库代码不需要特定上下文
    var response = await httpClient.GetAsync(url).ConfigureAwait(false);
    return await response.Content.ReadAsStringAsync().ConfigureAwait(false);
    // 性能更好,避免不必要的上下文切换
}

3️⃣ 组合任务:异步编程的核心

3.1 Task.WhenAll:并发执行多个任务

作用说明Task.WhenAll() 返回一个新的 Task,该任务会在所有传入的任务都完成后才完成。与 WaitAll 不同,WhenAll异步的,使用 await 等待时不会阻塞线程。如果传入的是 Task<T>[],则返回 Task<T[]>,包含所有任务的结果。如果任何任务失败,会在 await 时抛出第一个异常(完整异常信息在 task.Exception 中)。

场景:同时调用多个独立的 API。

async Task<string> GetUserAsync(int id)
{
    await Task.Delay(1000);
    return $"User{id}";
}

async Task<string> GetOrderAsync(int id)
{
    await Task.Delay(1500);
    return $"Order{id}";
}

// 串行执行:总耗时 2.5 秒
var user = await GetUserAsync(1);
var order = await GetOrderAsync(1);

//  并发执行:总耗时约 1.5 秒(取最长任务的时间)
Task<string> userTask = GetUserAsync(1);
Task<string> orderTask = GetOrderAsync(1);

string[] results = await Task.WhenAll(userTask, orderTask);
Console.WriteLine($"{results[0]}, {results[1]}");

处理不同类型的返回值

var userTask = GetUserAsync(1);      // Task<string>
var countTask = GetUserCountAsync(); // Task<int>

// 等待所有任务完成(但无法直接获取结果数组,因为类型不同)
await Task.WhenAll(userTask, countTask);

// 任务完成后,可以安全访问 Result(不会阻塞)
string user = userTask.Result;
int count = countTask.Result;

3.2 Task.WhenAny:响应最快的结果

作用说明Task.WhenAny() 返回一个新的 Task,该任务会在传入的任务中任意一个完成时就立即完成。返回值类型是 Task<Task>Task<Task<T>>,即”完成的那个任务本身”。与 WaitAny 不同,WhenAny异步的,不会阻塞线程。重要:方法返回后,其他未完成的任务不会被取消,它们会继续执行,除非你手动取消。

场景 1:超时控制

async Task<string> GetDataWithTimeoutAsync()
{
    Task<string> dataTask = GetDataFromSlowServiceAsync();
    Task delayTask = Task.Delay(3000); // 3 秒超时

    Task completedTask = await Task.WhenAny(dataTask, delayTask);

    if (completedTask == dataTask)
    {
        return await dataTask; // 数据任务先完成
    }
    else
    {
        throw new TimeoutException("请求超时");
        // 注意:dataTask 仍在后台执行,未被取消
    }
}

场景 2:多数据源竞速

async Task<string> GetFastestDataAsync()
{
    var source1 = GetDataFromSource1Async();
    var source2 = GetDataFromSource2Async();
    var source3 = GetDataFromSource3Async();

    // 哪个先返回用哪个
    Task<string> winner = await Task.WhenAny(source1, source2, source3);
    return await winner; // 获取最快的结果
    // 其他两个数据源的任务会继续执行,直到完成或程序退出
}

3.3 Task.WhenEach:逐个处理完成的任务(.NET 9+)

这是 .NET 9 新增的 API,用于按完成顺序处理任务。

作用说明Task.WhenEach() 返回一个 IAsyncEnumerable<Task<T>>,它会按照任务完成的顺序(而非创建顺序)逐个返回已完成的任务。这允许你在任务完成时立即处理它,而不是像 WhenAll 那样等待所有任务完成后一次性处理。所有任务仍然是并发执行的,只是处理顺序是按完成时间排序的。

#if NET9_0_OR_GREATER
async Task ProcessTasksAsTheyCompleteAsync()
{
    Task<int>[] tasks = new[]
    {
        Task.Run(async () => { await Task.Delay(2000); return 1; }),
        Task.Run(async () => { await Task.Delay(500); return 2; }),
        Task.Run(async () => { await Task.Delay(1000); return 3; })
    };

    // 按完成顺序处理(不是创建顺序)
    await foreach (var completedTask in Task.WhenEach(tasks))
    {
        int result = await completedTask;
        Console.WriteLine($"任务完成,结果: {result}");
    }
    // 输出顺序: 2(500ms后), 3(1000ms后), 1(2000ms后)
}
#endif

对比 Task.WhenAll

  • WhenAll:等待所有任务完成后一次性处理,总耗时 = 最长任务的时间
  • WhenEach:每完成一个立即处理,可以更早地开始后续操作,提升用户体验

4️⃣ 任务延续:ContinueWith vs await

4.1 ContinueWith:传统方式

作用说明ContinueWith() 用于在任务完成后执行后续操作(延续任务)。它接收一个委托,该委托的参数是前一个任务本身(antecedent),可以通过它获取结果、状态或异常。延续任务会在前一个任务完成后立即执行,可以在同一线程或不同线程上执行(取决于 TaskScheduler)。

Task.Run(() =>
{
    Thread.Sleep(1000);
    return 42;
})
.ContinueWith(antecedent =>
{
    int result = antecedent.Result;
    Console.WriteLine($"结果: {result}");
});

处理异常的复杂性

Task.Run(() =>
{
    throw new InvalidOperationException("错误");
})
.ContinueWith(antecedent =>
{
    if (antecedent.IsFaulted)
    {
        Console.WriteLine($"异常: {antecedent.Exception?.Message}");
    }
    else if (antecedent.IsCanceled)
    {
        Console.WriteLine("任务被取消");
    }
    else
    {
        Console.WriteLine($"结果: {antecedent.Result}");
    }
});

4.2 await:现代推荐方式

作用说明await 关键字会异步等待任务完成,然后直接返回结果(对于 Task<T>)或继续执行(对于 Task)。它使异步代码看起来像同步代码,异常可以通过标准的 try-catch 捕获。编译器会将其转换为状态机,自动处理线程调度和同步上下文。

try
{
    int result = await Task.Run(() =>
    {
        Thread.Sleep(1000);
        return 42;
    });

    Console.WriteLine($"结果: {result}");
}
catch (Exception ex)
{
    Console.WriteLine($"异常: {ex.Message}");
}

对比总结

特性 ContinueWith await
代码可读性 嵌套回调 线性代码
异常处理 复杂 try-catch
同步上下文 需手动控制 自动捕获
推荐度 ️ 特殊场景 日常使用

推荐:在现代代码中优先使用 awaitContinueWith 仅用于特殊场景(如需要指定 TaskScheduler)。


5️⃣ 任务状态:查询与判断

5.1 Status 属性:任务的生命周期

Task task = new Task(() =>
{
    Thread.Sleep(1000);
});

Console.WriteLine(task.Status); // Created

task.Start();
Console.WriteLine(task.Status); // Running 或 WaitingForActivation

task.Wait();
Console.WriteLine(task.Status); // RanToCompletion

完整的状态枚举

public enum TaskStatus
{
    Created,                // 已创建但未启动
    WaitingForActivation,   // 等待调度器激活
    WaitingToRun,          // 已排队等待执行
    Running,               // 正在执行
    WaitingForChildrenToComplete, // 等待子任务完成
    RanToCompletion,       // 成功完成
    Canceled,              // 已取消
    Faulted                // 发生异常
}

5.2 常用状态属性

Task<int> task = Task.Run(() =>
{
    Thread.Sleep(1000);
    return 42;
});

// 是否已完成(无论成功、失败还是取消)
bool isCompleted = task.IsCompleted;

// 是否成功完成
bool isSuccess = task.IsCompletedSuccessfully; // .NET Core 2.0+

// 是否发生异常
bool isFaulted = task.IsFaulted;

// 是否被取消
bool isCanceled = task.IsCanceled;

// 获取结果(阻塞等待)
if (task.IsCompletedSuccessfully)
{
    int result = task.Result;
}

// 获取异常
if (task.IsFaulted)
{
    AggregateException? exception = task.Exception;
}

5.3 实战:轮询任务状态(不推荐)

//  错误:浪费 CPU 的轮询
Task<int> task = Task.Run(() =>
{
    Thread.Sleep(2000);
    return 42;
});

while (!task.IsCompleted)
{
    Console.WriteLine("等待中...");
    Thread.Sleep(100); // 浪费 CPU
}

Console.WriteLine($"结果: {task.Result}");

正确:使用 await 或 Wait

Task<int> task = Task.Run(() =>
{
    Thread.Sleep(2000);
    return 42;
});

int result = await task; // 或 task.Wait();
Console.WriteLine($"结果: {result}");

6️⃣ 高级主题:TaskScheduler 与 TaskFactory

6.1 TaskScheduler:控制任务的执行位置

// 默认调度器:使用线程池
Task.Run(() => Console.WriteLine("线程池执行"));

// 获取当前调度器
TaskScheduler current = TaskScheduler.Current;
TaskScheduler defaultScheduler = TaskScheduler.Default;

// 在 UI 线程上执行(WPF/WinForms)
var uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();
Task.Factory.StartNew(() =>
{
    // 这里的代码在 UI 线程执行
    // button1.Text = "更新";
}, CancellationToken.None,
   TaskCreationOptions.None,
   uiScheduler);

6.2 TaskFactory:批量创建任务

TaskFactory 允许你创建一个带有默认配置的任务工厂,用于批量创建具有相同设置的任务。

TaskFactory 构造函数详解

TaskFactory factory = new TaskFactory(
    CancellationToken.None,              // 参数 1: 取消令牌
    TaskCreationOptions.LongRunning,     // 参数 2: 任务创建选项
    TaskContinuationOptions.None,        // 参数 3: 延续选项
    TaskScheduler.Default                // 参数 4: 任务调度器
);

构造函数参数说明

参数 类型 说明 常用值
cancellationToken CancellationToken 用于取消工厂创建的所有任务 CancellationToken.None (不取消)
cts.Token (支持取消)
creationOptions TaskCreationOptions 任务创建时的行为选项 NoneLongRunningAttachedToParent
continuationOptions TaskContinuationOptions 延续任务的默认选项 NoneOnlyOnRanToCompletion
scheduler TaskScheduler 任务调度器,决定任务在哪里执行 TaskScheduler.Default (ThreadPool)
TaskScheduler.FromCurrentSynchronizationContext() (UI 线程)

TaskCreationOptions 枚举详解

这是最重要的配置选项,直接影响任务的执行方式和性能。

[Flags]
public enum TaskCreationOptions
{
    None = 0x0,                    // 默认:无特殊选项
    PreferFairness = 0x1,          // 优先公平性(FIFO 调度)
    LongRunning = 0x2,             // 长时间运行(使用独立线程)
    AttachedToParent = 0x4,        // 附加到父任务
    DenyChildAttach = 0x8,         // 拒绝子任务附加
    HideScheduler = 0x10,          // 隐藏调度器(防止子任务继承)
    RunContinuationsAsynchronously = 0x40  // 异步运行延续
}
1️⃣ None(默认值)
Task.Run(() => DoWork());  // 等价于 TaskCreationOptions.None
  • 行为:使用 ThreadPool 线程,默认调度策略(工作窃取)
  • 适用场景:99% 的普通任务
  • 性能特点:高效,线程复用

2️⃣ LongRunning(长时间运行)
Task.Factory.StartNew(() => 
{
    while (true)
    {
        // 长时间运行的循环
        Thread.Sleep(1000);
    }
}, TaskCreationOptions.LongRunning);

详细说明

  • 行为创建独立的专用线程,而不是使用 ThreadPool 线程
  • 线程类型new Thread() 创建的后台线程
  • 何时使用
    • 任务运行时间超过 1 秒以上
    • 阻塞式操作(如 Thread.SleepBlockingCollection.Take()
    • 无限循环的后台服务(如消息队列监听)
  • 性能影响
    • 不占用 ThreadPool 线程(避免线程池饥饿)
    • ️ 创建线程有开销(~1ms + 1MB 栈内存)
    • ️ 不适合大量短任务(会创建大量线程)

️ 常见误用

//  错误:短任务使用 LongRunning(性能浪费)
Task.Factory.StartNew(() => 
{
    Console.WriteLine("快速任务");
}, TaskCreationOptions.LongRunning);  // 创建线程的开销比任务本身还大!

//  正确:使用默认选项
Task.Run(() => Console.WriteLine("快速任务"));

3️⃣ AttachedToParent(附加到父任务)
Task parent = Task.Factory.StartNew(() =>
{
    Console.WriteLine("父任务开始");

    // 子任务附加到父任务
    Task child = Task.Factory.StartNew(() =>
    {
        Thread.Sleep(2000);
        Console.WriteLine("子任务完成");
    }, TaskCreationOptions.AttachedToParent);

    Console.WriteLine("父任务逻辑完成");
});

parent.Wait();  // ️ 会等待子任务也完成!
Console.WriteLine("父任务真正完成");

行为

  • 父任务的 IsCompleted 会等待所有附加的子任务完成
  • 子任务的异常会传播到父任务
  • 父任务的取消会传播到子任务

执行输出

父任务开始
父任务逻辑完成
(等待 2 秒)
子任务完成
父任务真正完成

️ 现代代码中很少使用

  • async/await 提供了更清晰的层次结构
  • 异常传播容易混乱
  • 调试困难

4️⃣ DenyChildAttach(拒绝子任务附加)
Task parent = Task.Factory.StartNew(() =>
{
    // 即使子任务使用 AttachedToParent,也不会附加
    Task child = Task.Factory.StartNew(() =>
    {
        Thread.Sleep(2000);
        Console.WriteLine("子任务完成");
    }, TaskCreationOptions.AttachedToParent);  // 无效!被拒绝

    Console.WriteLine("父任务完成");
}, TaskCreationOptions.DenyChildAttach);

parent.Wait();  // 立即返回,不等待子任务

用途

  • 防止第三方库的代码意外附加子任务
  • 保证任务独立性

5️⃣ PreferFairness(优先公平性)
for (int i = 0; i < 10; i++)
{
    int id = i;
    Task.Factory.StartNew(() => 
    {
        Console.WriteLine($"任务 {id}");
    }, TaskCreationOptions.PreferFairness);
}

详细说明

  • 行为:使用 FIFO(先进先出)队列 调度任务,而不是默认的工作窃取队列
  • 默认调度(无此选项):
    • ThreadPool 使用工作窃取算法
    • 新任务优先放入本地队列(LIFO)
    • 其他线程可以窃取(FIFO)
    • 优势:缓存友好,性能高
  • FIFO 调度(此选项):
    • 所有任务放入全局队列
    • 严格按提交顺序执行
    • 优势:公平,避免某些任务饥饿
    • 劣势:性能略低(全局队列竞争)

何时使用

  • 任务执行顺序很重要
  • 需要避免某些任务长期得不到执行
  • ️ 性能不是首要考虑

性能对比

默认(工作窃取):    任务 0 → 任务 2 → 任务 1 → 任务 4 → 任务 3 ...(乱序,高性能)
PreferFairness:      任务 0 → 任务 1 → 任务 2 → 任务 3 → 任务 4 ...(顺序,公平)

6️⃣ HideScheduler(隐藏调度器)
TaskScheduler customScheduler = new CustomTaskScheduler();

Task parent = Task.Factory.StartNew(() =>
{
    // 子任务不会继承 customScheduler
    Task child = Task.Run(() => 
    {
        Console.WriteLine($"子任务调度器:{TaskScheduler.Current}");
        // 输出:System.Threading.Tasks.TaskScheduler+ThreadPoolTaskScheduler
    });
}, CancellationToken.None, TaskCreationOptions.HideScheduler, customScheduler);

用途

  • 防止子任务继承父任务的自定义调度器
  • 确保子任务在默认 ThreadPool 上执行

7️⃣ RunContinuationsAsynchronously(异步运行延续)
var tcs = new TaskCompletionSource<int>(
    TaskCreationOptions.RunContinuationsAsynchronously);

tcs.Task.ContinueWith(t => 
{
    Console.WriteLine($"延续运行在线程:{Thread.CurrentThread.ManagedThreadId}");
});

// 设置结果
Console.WriteLine($"主线程:{Thread.CurrentThread.ManagedThreadId}");
tcs.SetResult(42);  // 延续会在 ThreadPool 线程执行,而不是当前线程

详细说明

  • 默认行为(无此选项):
    • 延续(ContinueWith)会在 调用 SetResult/SetException 的线程 上同步执行
    • 风险:可能阻塞调用者
  • 此选项行为
    • 延续会排队到 ThreadPool,异步执行
    • 调用 SetResult 的线程不会被阻塞

何时使用

  • 使用 TaskCompletionSource
  • 延续可能执行耗时操作
  • 避免阻塞调用 SetResult 的线程

组合使用(Flags 枚举)

TaskCreationOptions 是 Flags 枚举,可以组合多个选项:

Task.Factory.StartNew(() => 
{
    // 长时间运行 + 拒绝子任务附加
}, TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach);

实际应用示例

示例 1:长时间运行的后台服务

var cts = new CancellationTokenSource();

Task.Factory.StartNew(() =>
{
    while (!cts.Token.IsCancellationRequested)
    {
        // 处理消息队列
        ProcessMessages();
        Thread.Sleep(100);
    }
}, cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);

// 稍后取消
cts.Cancel();

示例 2:批量创建相同配置的任务

// 创建工厂:所有任务都是长时间运行 + 支持取消
var cts = new CancellationTokenSource();
TaskFactory factory = new TaskFactory(
    cts.Token,
    TaskCreationOptions.LongRunning,
    TaskContinuationOptions.None,
    TaskScheduler.Default
);

// 批量创建
var tasks = new List<Task>();
for (int i = 0; i < 5; i++)
{
    int id = i;
    tasks.Add(factory.StartNew(() => 
    {
        while (!cts.Token.IsCancellationRequested)
        {
            Console.WriteLine($"工作线程 {id}");
            Thread.Sleep(1000);
        }
    }));
}

// 稍后统一取消
cts.Cancel();
await Task.WhenAll(tasks);

现代代码建议

在现代 C# 代码中

  • 99% 的情况使用 Task.Run(默认 TaskCreationOptions.None
  • 长时间运行的阻塞操作:使用 TaskCreationOptions.LongRunning
  • 需要取消功能:传递 CancellationToken,而不是使用 TaskFactory
  • 避免使用 AttachedToParent(用 async/await 代替)
  • 避免使用 TaskFactory(除非批量创建相同配置的任务)

推荐写法

//  现代写法(简洁清晰)
await Task.Run(() => DoWork(), cancellationToken);

//  旧式写法(冗长复杂)
TaskFactory factory = new TaskFactory(cancellationToken, ...);
await factory.StartNew(() => DoWork());

关键要点:TaskFactory 主要用于遗留代码或需要精细控制任务创建的场景。日常开发中,Task.Run + async/await 是更好的选择。


7️⃣ 常见陷阱与最佳实践

陷阱 1:滥用同步等待

//  极其危险:死锁风险(WPF/WinForms)
public void Button_Click(object sender, EventArgs e)
{
    var result = GetDataAsync().Result; // 死锁!
}

async Task<string> GetDataAsync()
{
    await Task.Delay(1000);
    return "数据";
}

死锁原因

  1. GetDataAsync 在 UI 线程启动
  2. await Task.Delay 注册延续回 UI 线程
  3. .Result 阻塞 UI 线程
  4. 延续无法执行 → 死锁

解决方案

// 方案 1:使用 async 一路到底(强烈推荐)
public async void Button_Click(object sender, EventArgs e)
{
    var result = await GetDataAsync();
}

// 方案 2:使用 ConfigureAwait(false)(第5章详解)
async Task<string> GetDataAsync()
{
    await Task.Delay(1000).ConfigureAwait(false);
    return "数据";
}

陷阱 1.5:同步方法中调用异步方法的最佳实践

场景:有时你无法将整个调用链改为 async(如遗留代码、同步接口实现),必须在同步方法中调用异步方法。

//  最差:使用 .Result(死锁风险 + 包装异常)
public string GetData()
{
    return GetDataAsync().Result;  // AggregateException + 死锁
}

// ️ 较差:使用 .Wait()(死锁风险)
public void ProcessData()
{
    GetDataAsync().Wait();  // 死锁风险
}

// ️ 相对安全:ConfigureAwait(false).GetAwaiter().GetResult()
public string GetData()
{
    return GetDataAsync()
        .ConfigureAwait(false)  // 不捕获上下文,降低死锁风险
        .GetAwaiter()
        .GetResult();           // 直接抛出原始异常,而非 AggregateException
}

//  最佳:改为异步方法
public async Task<string> GetDataAsync()
{
    return await GetDataAsync();
}

为什么 .ConfigureAwait(false).GetAwaiter().GetResult() 相对安全?

  1. ConfigureAwait(false)

    • 不捕获 SynchronizationContext
    • 避免在 UI 线程等待时的死锁(但不是 100% 保证)
  2. GetAwaiter().GetResult()

    • 直接抛出原始异常(如 InvalidOperationException
    • 而不是包装在 AggregateException
    • 异常堆栈更清晰

️ 但这仍然不是最佳实践

  • 仍然阻塞线程(浪费 ThreadPool 资源)
  • 在某些复杂场景下仍可能死锁
  • 最佳解决方案:将整个调用链改为 async/await

何时可以接受使用

  1. 遗留代码迁移(无法改为 async):

    // 旧的同步接口
    public interface IDataRepository
    {
        string GetData();  // 无法改为 async
    }
    
    // 实现时调用异步方法
    public class DataRepository : IDataRepository
    {
        public string GetData()
        {
            return GetDataAsync()
                .ConfigureAwait(false)
                .GetAwaiter()
                .GetResult();
        }
    
        private async Task<string> GetDataAsync() { ... }
    }
    
  2. Main 方法(.NET Framework):

    // .NET Framework(无 top-level statements)
    static void Main(string[] args)
    {
        MainAsync(args)
            .ConfigureAwait(false)
            .GetAwaiter()
            .GetResult();
    }
    
    static async Task MainAsync(string[] args)
    {
        await DoWorkAsync();
    }
    
  3. 同步 Dispose(但更推荐 IAsyncDisposable):

    public class MyClass : IDisposable
    {
        public void Dispose()
        {
            DisposeAsync()
                .ConfigureAwait(false)
                .GetAwaiter()
                .GetResult();
        }
    
        public async ValueTask DisposeAsync()
        {
            await CleanupAsync();
        }
    }
    

详细原理:关于 SynchronizationContext、上下文捕获、死锁机制的深入讲解,请参考第 05 章《SynchronizationContext 深入解析》。


陷阱 2:忘记 await 或 Wait

//  错误:任务启动但没有等待
public void DoWork()
{
    Task.Run(() =>
    {
        throw new Exception("错误");
    });

    Console.WriteLine("继续执行");
    // 异常被吞掉,没有人观察到!
}

正确

public async Task DoWorkAsync()
{
    await Task.Run(() =>
    {
        throw new Exception("错误");
    });

    Console.WriteLine("继续执行");
}

陷阱 3:在循环中创建任务导致闭包问题

//  错误:所有任务都打印 10
var tasks = new List<Task>();
for (int i = 0; i < 10; i++)
{
    tasks.Add(Task.Run(() => Console.WriteLine(i)));
}
Task.WaitAll(tasks.ToArray());

正确

var tasks = new List<Task>();
for (int i = 0; i < 10; i++)
{
    int localI = i; // 捕获局部变量
    tasks.Add(Task.Run(() => Console.WriteLine(localI)));
}
Task.WaitAll(tasks.ToArray());

陷阱 4:误用 Task.Run 包装已经是异步的方法

//  错误:双重异步,浪费资源
public Task<string> GetDataAsync()
{
    return Task.Run(async () =>
    {
        return await httpClient.GetStringAsync("https://api.example.com");
    });
}

//  正确:直接返回
public Task<string> GetDataAsync()
{
    return httpClient.GetStringAsync("https://api.example.com");
}

原则

  • Task.Run 用于 CPU 密集型 操作
  • I/O 操作(网络、文件)本身就是异步的,无需 Task.Run

8️⃣ 实战演练:综合示例

示例 1:并发下载多个文件

async Task DownloadFilesAsync(string[] urls)
{
    using HttpClient client = new HttpClient();

    // 创建所有下载任务
    Task<string>[] tasks = urls.Select(url => 
        client.GetStringAsync(url)
    ).ToArray();

    // 并发执行
    string[] results = await Task.WhenAll(tasks);

    for (int i = 0; i < results.Length; i++)
    {
        Console.WriteLine($"文件 {i + 1}: {results[i].Length} 字节");
    }
}

示例 2:带重试的任务执行

async Task<T> ExecuteWithRetryAsync<T>(
    Func<Task<T>> operation,
    int maxRetries = 3)
{
    for (int i = 0; i < maxRetries; i++)
    {
        try
        {
            return await operation();
        }
        catch (Exception ex) when (i < maxRetries - 1)
        {
            Console.WriteLine($"第 {i + 1} 次失败: {ex.Message}");
            await Task.Delay(1000 * (i + 1)); // 递增延迟
        }
    }

    throw new InvalidOperationException("达到最大重试次数");
}

// 使用
var result = await ExecuteWithRetryAsync(async () =>
{
    return await httpClient.GetStringAsync("https://api.example.com");
});

示例 3:批量处理的限流控制

async Task ProcessItemsWithThrottleAsync<T>(
    IEnumerable<T> items,
    Func<T, Task> processor,
    int maxConcurrency = 5)
{
    using SemaphoreSlim semaphore = new SemaphoreSlim(maxConcurrency);

    var tasks = items.Select(async item =>
    {
        await semaphore.WaitAsync();
        try
        {
            await processor(item);
        }
        finally
        {
            semaphore.Release();
        }
    });

    await Task.WhenAll(tasks);
}

// 使用:最多同时处理 5 个
await ProcessItemsWithThrottleAsync(
    Enumerable.Range(1, 100),
    async i =>
    {
        await Task.Delay(1000);
        Console.WriteLine($"处理 {i}");
    },
    maxConcurrency: 5
);

本章总结:Task API 快速参考卡片

全面掌握 Task 类的核心 API – 建议将此速查表加入书签或打印出来作为日常开发参考


创建任务

API 说明 使用场景 示例
Task.Run(() => {}) 立即启动任务 日常首选 Task.Run(() => DoWork())
Task.Run<T>(() => {}) 带返回值的任务 需要结果时 Task.Run(() => 42)
Task.Factory.StartNew() 高级控制 ️ 特殊场景 Task.Factory.StartNew(() => {}, TaskCreationOptions.LongRunning)
new Task(() => {}) + Start() 手动启动 ️ 延迟启动 var task = new Task(() => {}); task.Start();
Task.FromResult(value) 已完成的任务 缓存/优化 Task.FromResult(42)
Task.CompletedTask 已完成的空任务 接口实现 return Task.CompletedTask;
Task.FromCanceled(token) 已取消的任务 ️ 特殊场景 Task.FromCanceled(cancellationToken)
Task.FromException(ex) 已失败的任务 ️ 特殊场景 Task.FromException(new Exception())

⏳ 等待任务

API 说明 阻塞/异步 推荐度 示例
await task 异步等待 异步 推荐 await GetDataAsync()
task.Wait() 阻塞等待 阻塞 ️ 控制台可用 task.Wait()
task.Wait(timeout) 带超时等待 阻塞 ️ 特殊场景 task.Wait(5000)
Task.WaitAll(tasks) 等待所有任务 阻塞 ️ 用 WhenAll Task.WaitAll(task1, task2)
Task.WaitAny(tasks) 等待任意一个 阻塞 ️ 用 WhenAny Task.WaitAny(task1, task2)
task.Result 获取结果 阻塞 避免 int result = task.Result;

组合任务

API 说明 返回类型 使用场景
Task.WhenAll(tasks) 等待所有完成 TaskTask<T[]> 并发执行多个任务
Task.WhenAny(tasks) 等待任意一个 Task<Task> 超时控制、竞速
Task.WhenEach(tasks) 逐个处理完成的任务 (.NET 9+) IAsyncEnumerable<Task<T>> 实时处理

示例对比

// WhenAll - 等待所有任务
var results = await Task.WhenAll(task1, task2, task3);
// results = [result1, result2, result3]

// WhenAny - 响应最快的
var firstTask = await Task.WhenAny(task1, task2, task3);
var result = await firstTask;

// WhenEach - 按完成顺序处理 (.NET 9+)
await foreach (var task in Task.WhenEach(task1, task2, task3))
{
    var result = await task;
    // 处理每个完成的任务
}

任务状态

属性 类型 说明
task.Status TaskStatus 枚举 任务当前状态
task.IsCompleted bool 是否已完成(成功/失败/取消)
task.IsCompletedSuccessfully bool 是否成功完成 (.NET Core 2.0+)
task.IsFaulted bool 是否发生异常
task.IsCanceled bool 是否被取消
task.Exception AggregateException? 异常信息(如果有)
task.Result T 任务结果(️ 阻塞)

TaskStatus 枚举值

Created                      // 已创建但未启动
WaitingForActivation        // 等待调度器激活
WaitingToRun                // 已排队等待执行
Running                     // 正在执行
WaitingForChildrenToComplete // 等待子任务完成
RanToCompletion             //  成功完成
Canceled                    // ️ 已取消
Faulted                     //  发生异常

任务延续

API 代码风格 推荐度 说明
await 线性 推荐 现代异步编程标准
ContinueWith 回调 ️ 特殊场景 复杂、异常处理麻烦
//  ContinueWith (不推荐)
task.ContinueWith(t =>
{
    if (t.IsFaulted) { /* 处理异常 */ }
    else { var result = t.Result; }
});

//  await (推荐)
try
{
    var result = await task;
}
catch (Exception ex)
{
    // 处理异常
}

️ 常见陷阱速查

陷阱 问题 解决方案
忘记 await/Wait 异常被吞掉 总是 await 或 Wait 任务
循环闭包 所有任务捕获相同的变量 使用局部变量:int local = i;
双重异步 Task.Run(async () => await ...) I/O 操作直接 await,不要用 Task.Run
UI 死锁 .Result / .Wait() 在 UI 线程 使用 async/await 一路到底
ASP.NET 阻塞 .Result 浪费线程 控制器方法使用 async Task

最佳实践速查

推荐做法

  1. 创建任务:使用 Task.Run(CPU 密集型)
  2. 等待任务:使用 await(异步方法中)
  3. 并发执行:使用 Task.WhenAll
  4. 超时控制:使用 Task.WhenAny
  5. 异常处理:使用 try-catch 包裹 await

避免做法

  1. 在 UI/ASP.NET 中使用 .Result.Wait()
  2. 忘记 await 导致异常被忽略
  3. Task.Run 包装已经是异步的方法
  4. 在循环中直接捕获循环变量
  5. 使用轮询检查 IsCompleted

何时用什么?

需要并发执行 CPU 密集型操作?
  → Task.Run

需要等待多个任务完成?
  → await Task.WhenAll(tasks)

需要超时控制?
  → await Task.WhenAny(task, Task.Delay(timeout))

需要重试逻辑?
  → 自己实现重试循环 + await

需要限制并发数?
  → SemaphoreSlim + Task.WhenAll

I/O 操作(网络、文件)?
  → 直接使用异步 API,不要用 Task.Run

核心要点回顾

创建任务

  • 优先使用 Task.Run
  • Task.Factory.StartNew 仅用于高级场景
  • 避免 new Task() 忘记 Start()

等待任务

  • 异步代码中使用 await
  • 避免在 UI/ASP.NET 中使用 .Result.Wait()
  • 并发用 WhenAll,竞速用 WhenAny

任务组合

  • Task.WhenAll:等待所有任务完成
  • Task.WhenAny:响应最快的任务
  • Task.WhenEach:按完成顺序处理(.NET 9+)

状态查询

  • IsCompletedIsFaultedIsCanceled
  • 避免轮询状态,使用 awaitWait

最佳实践

  • 异步一路到底(async all the way)
  • CPU 密集型用 Task.Run,I/O 操作直接用异步 API
  • 注意闭包陷阱和死锁风险

下一章预告

在下一章《async/await 原理与性能优化》中,我们将深入探讨:

  • async/await 的编译器魔法:状态机是如何生成的?
  • 为什么 async/await 不等于多线程?
  • 如何通过 ValueTask 优化性能?
  • 同步上下文的捕获与恢复

推荐练习

  1. 实现一个支持超时和重试的 HTTP 请求方法
  2. 使用 Task.WhenAll 并发调用多个 API
  3. 对比 Task.Run 和直接使用异步 API 的性能差异

参考资源

官方文档与源码

  1. Task 类源码(.NET Runtime)

    • System.Threading.Tasks.Task
    • Microsoft 官方开源的 Task 实现,可以深入了解底层细节
    • 推荐关注:InternalWaitContinueWith、状态机相关代码
  2. 官方文档 – 基于任务的异步模式(TAP)


文章摘自:https://www.cnblogs.com/diamondhusky/p/19891861