查看: 108|回覆: 0

【.NET并发编程 - 03】 Task API 完全指南:方法与属性的实战应用

[複製鏈接]

5

主題

0

回帖

0

積分

热心网友

金币
0
閲讀權限
220
精華
0
威望
0
贡献
0
在線時間
0 小時
註冊時間
2011-11-5
發表於 2026-4-19 22:31:00 | 顯示全部樓層 |閲讀模式

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.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)

    • Task-based Asynchronous Pattern (TAP)
    • 微软官方的异步编程模式指南
    • 包含最佳实践、性能优化建议



来源:https://www.cnblogs.com/diamondhusky/p/19891861
回覆

使用道具 舉報

您需要登錄後才可以回帖 登錄 | 立即注册

本版積分規則

相关侵权、举报、投诉及建议等,请发 E-mail:qiongdian@foxmail.com

Powered by Discuz! X5.0 © 2001-2026 Discuz! Team.

在本版发帖返回顶部