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 个时,新请求必须排队等待线程释放
- 排队导致延迟增加,用户体验下降
- 严重时可能导致请求超时、服务不可用
为什么会造成性能崩溃?
- 线程饥饿:ThreadPool 的线程被大量阻塞操作占据,无法处理新请求
- 内存浪费:每个阻塞线程占用 ~1MB 栈空间,但完全空闲
- 延迟雪崩:排队等待的请求越来越多,延迟指数级增长
- 资源死锁风险:在复杂场景下,可能导致所有线程互相等待
提示:这就是为什么在高并发场景下,绝对不能使用
.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() 返回一个 TaskAwaiter 或 TaskAwaiter<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 |
| 同步上下文 | 需手动控制 | 自动捕获 |
| 推荐度 | ️ 特殊场景 | 日常使用 |
推荐:在现代代码中优先使用 await,ContinueWith 仅用于特殊场景(如需要指定 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 |
任务创建时的行为选项 | None、LongRunning、AttachedToParent 等 |
| continuationOptions | TaskContinuationOptions |
延续任务的默认选项 | None、OnlyOnRanToCompletion 等 |
| 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.Sleep、BlockingCollection.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 "数据";
}
死锁原因:
GetDataAsync在 UI 线程启动await Task.Delay注册延续回 UI 线程.Result阻塞 UI 线程- 延续无法执行 → 死锁
解决方案:
// 方案 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() 相对安全?
-
ConfigureAwait(false):
- 不捕获
SynchronizationContext - 避免在 UI 线程等待时的死锁(但不是 100% 保证)
- 不捕获
-
GetAwaiter().GetResult():
- 直接抛出原始异常(如
InvalidOperationException) - 而不是包装在
AggregateException中 - 异常堆栈更清晰
- 直接抛出原始异常(如
️ 但这仍然不是最佳实践:
- 仍然阻塞线程(浪费 ThreadPool 资源)
- 在某些复杂场景下仍可能死锁
- 最佳解决方案:将整个调用链改为
async/await
何时可以接受使用:
-
遗留代码迁移(无法改为 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() { ... } } -
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(); } -
同步 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) |
等待所有完成 | Task 或 Task<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 |
最佳实践速查
推荐做法:
- 创建任务:使用
Task.Run(CPU 密集型) - 等待任务:使用
await(异步方法中) - 并发执行:使用
Task.WhenAll - 超时控制:使用
Task.WhenAny - 异常处理:使用 try-catch 包裹 await
避免做法:
- 在 UI/ASP.NET 中使用
.Result或.Wait() - 忘记 await 导致异常被忽略
- 用
Task.Run包装已经是异步的方法 - 在循环中直接捕获循环变量
- 使用轮询检查
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+)
状态查询:
IsCompleted、IsFaulted、IsCanceled- 避免轮询状态,使用
await或Wait
最佳实践:
- 异步一路到底(async all the way)
- CPU 密集型用
Task.Run,I/O 操作直接用异步 API - 注意闭包陷阱和死锁风险
下一章预告
在下一章《async/await 原理与性能优化》中,我们将深入探讨:
- async/await 的编译器魔法:状态机是如何生成的?
- 为什么 async/await 不等于多线程?
- 如何通过 ValueTask 优化性能?
- 同步上下文的捕获与恢复
推荐练习:
- 实现一个支持超时和重试的 HTTP 请求方法
- 使用
Task.WhenAll并发调用多个 API - 对比
Task.Run和直接使用异步 API 的性能差异
参考资源
官方文档与源码
-
Task 类源码(.NET Runtime)
- System.Threading.Tasks.Task
- Microsoft 官方开源的 Task 实现,可以深入了解底层细节
- 推荐关注:
InternalWait、ContinueWith、状态机相关代码
-
官方文档 – 基于任务的异步模式(TAP)
- Task-based Asynchronous Pattern (TAP)
- 微软官方的异步编程模式指南
- 包含最佳实践、性能优化建议
文章摘自:https://www.cnblogs.com/diamondhusky/p/19891861
